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,1402 @@
|
|
|
1
|
+
"""MiniZinc/CP finite-domain refutation search — the second finite-domain backend.
|
|
2
|
+
|
|
3
|
+
:class:`MinizincBackend` decides ``premises ⊨ φ`` the same way
|
|
4
|
+
:class:`~unicode_logic_kit.atp.clingo_backend.ClingoBackend` does (built
|
|
5
|
+
alongside this module, not in it): both ground ``premises ∧ ¬φ`` over a
|
|
6
|
+
bounded domain ``0 … size-1`` and search for a satisfying assignment through
|
|
7
|
+
:mod:`~unicode_logic_kit.atp.finite_domain`'s shared problem/verification
|
|
8
|
+
layer. A model at some size REFUTES ``φ``; no model up to ``max_size``
|
|
9
|
+
is UNKNOWN/``"bound_hit"`` — see :mod:`atp.finite_domain`'s module docstring
|
|
10
|
+
for the one rule neither backend may soften: **this search never returns
|
|
11
|
+
PROVED**. Every reconstructed countermodel is re-checked by
|
|
12
|
+
:func:`~unicode_logic_kit.atp.finite_domain.verify_model` before it is allowed
|
|
13
|
+
to leave :meth:`MinizincBackend.decide` as REFUTED — and what
|
|
14
|
+
:mod:`atp.finite_domain`'s module docstring used to call a "Known
|
|
15
|
+
verification gap" for ``Cardinality``/``Function`` is CLOSED for both now,
|
|
16
|
+
the same way, in two steps. ``verify_model``'s own evaluator counts
|
|
17
|
+
``Cardinality`` comparisons arithmetically, so the counting fragment is
|
|
18
|
+
CLOSED end to end: a REFUTED verdict on e.g. ``|{x : P(x)}| > |{y : Q(y)}|``
|
|
19
|
+
carries a countermodel this backend has genuinely re-verified, not one that
|
|
20
|
+
downgrades to ERROR/``"infra"`` for want of a checker. ``Function`` is
|
|
21
|
+
CLOSED too, by the identical mechanism rather than by staying refused:
|
|
22
|
+
:func:`~unicode_logic_kit.atp.finite_domain.fragment_check` — consulted by
|
|
23
|
+
:func:`to_minizinc` before it emits a single declaration — now ADMITS a
|
|
24
|
+
``Function`` node generally (a *sorted* ``FunctionDecl`` stays refused, but
|
|
25
|
+
separately — see
|
|
26
|
+
:mod:`~unicode_logic_kit.atp.finite_domain`'s "Sorted function symbols"
|
|
27
|
+
section, not this gate), and ``verify_model``'s evaluator now reads
|
|
28
|
+
``f(t1,...,tk)`` off the SAME ``(name, arity+1)`` total-relation extension
|
|
29
|
+
:func:`~unicode_logic_kit.atp.finite_domain.structure_from_solution` already
|
|
30
|
+
reconstructs, so a function-bearing REFUTED verdict is genuinely
|
|
31
|
+
re-verified too. This module's OWN function-encoding machinery — the array
|
|
32
|
+
declarations in :func:`to_minizinc`, :func:`_term`'s ``Function`` branch,
|
|
33
|
+
:func:`_atoms_from_solution`'s function-decoding loop — is consequently
|
|
34
|
+
REACHABLE from :meth:`MinizincBackend.decide` now, not merely kept in place
|
|
35
|
+
as a second line of defence — see "Encoding — arrays and generators" below,
|
|
36
|
+
which used to document this as dormant and now documents it as live.
|
|
37
|
+
|
|
38
|
+
Many-sorted input
|
|
39
|
+
-------------------
|
|
40
|
+
:meth:`MinizincBackend.decide` builds ``sentences = tuple(premises) +
|
|
41
|
+
(Not(formula),)`` and then, before ``Signature.from_formulas`` or
|
|
42
|
+
:func:`to_minizinc`'s own :func:`~unicode_logic_kit.atp.finite_domain.fragment_check`
|
|
43
|
+
call ever see it, runs the whole batch through
|
|
44
|
+
:func:`~unicode_logic_kit.atp.finite_domain.lower_msfol` — a no-op for every
|
|
45
|
+
plain-FOL caller, and for a many-sorted one a relativisation to classical
|
|
46
|
+
FOL (plus one non-emptiness sentence per distinct sort name) that needs no
|
|
47
|
+
support from this module's own renderer or output parser at all: a sort
|
|
48
|
+
name is, after lowering, an ordinary unary predicate like any other. See
|
|
49
|
+
:mod:`~unicode_logic_kit.atp.finite_domain`'s own "Many-sorted input" section
|
|
50
|
+
for the full design, including why the non-emptiness sentence is required
|
|
51
|
+
for soundness against :mod:`~unicode_logic_kit.semantics.modelfinder`, the
|
|
52
|
+
oracle this is differentially tested against (offline here, since MiniZinc
|
|
53
|
+
itself is not installed in this environment — see below).
|
|
54
|
+
|
|
55
|
+
External binary, not a Python package
|
|
56
|
+
--------------------------------------
|
|
57
|
+
The originating design note for this backend (§6) mentions ``pip install
|
|
58
|
+
minizinc`` — the *Python* MiniZinc bindings. This module does NOT use that
|
|
59
|
+
package. Per the task this module was built under, discovery instead follows
|
|
60
|
+
the exact convention :class:`~unicode_logic_kit.atp.protocol.Prover9Backend`
|
|
61
|
+
and :class:`~unicode_logic_kit.atp.protocol.VampireBackend` already use for
|
|
62
|
+
their own external binaries: ``$UFK_MINIZINC``, then ``PATH``
|
|
63
|
+
(:func:`_minizinc_binary`). The generated ``.mzn`` text is written to a temp
|
|
64
|
+
file and handed to the ``minizinc`` CLI directly via ``subprocess`` — one
|
|
65
|
+
less runtime dependency, and one fewer translation layer between this
|
|
66
|
+
module's own encoding and what actually gets solved. MiniZinc is NOT
|
|
67
|
+
installed in this repository's development environment and will not be, so
|
|
68
|
+
:meth:`MinizincBackend.decide`'s live subprocess path is exercised only
|
|
69
|
+
through :func:`minizinc_available` returning ``False`` here; what IS fully
|
|
70
|
+
exercised and pinned by fixtures is everything this module can control
|
|
71
|
+
without a solver: :func:`to_minizinc` (the renderer) and the output parser
|
|
72
|
+
(:func:`_parse_minizinc_solution` / :func:`_atoms_from_solution`) — the same
|
|
73
|
+
split :mod:`atp.eprover_backend` / :mod:`atp.twee_backend` use for tools this
|
|
74
|
+
environment cannot run either.
|
|
75
|
+
|
|
76
|
+
Encoding — arrays and generators, not the ASP boolean-relation reading
|
|
77
|
+
--------------------------------------------------------------------------
|
|
78
|
+
This section documents the renderer's OWN convention for a function symbol —
|
|
79
|
+
LIVE now, on every ordinary search that mentions one, per the module
|
|
80
|
+
docstring's opening: :func:`fragment_check` admits ``Function`` generally,
|
|
81
|
+
so :meth:`MinizincBackend.decide` reaches this code on a genuine
|
|
82
|
+
function-bearing sentence today, not merely on the hand-built-problem
|
|
83
|
+
second-line-of-defence case the sorted/arithmetic refusals below still
|
|
84
|
+
describe.
|
|
85
|
+
:mod:`atp.finite_domain`'s module docstring describes the shared "function =
|
|
86
|
+
total relation + functionality constraint" reconstruction that
|
|
87
|
+
:func:`~unicode_logic_kit.atp.finite_domain.structure_from_solution` expects
|
|
88
|
+
BACK from a solver (a ``(k+1)``-tuple per function atom). That is the shape
|
|
89
|
+
this module's OUTPUT must produce, not necessarily the shape its INPUT
|
|
90
|
+
encoding has to take to produce it — and for MiniZinc the natural CP
|
|
91
|
+
encoding of a function symbol ``f`` of arity ``k`` is
|
|
92
|
+
``array[DOM, …, DOM] of var DOM: f_name;`` (``k`` copies of ``DOM``): a
|
|
93
|
+
MiniZinc array is by construction both TOTAL (every index combination in its
|
|
94
|
+
declared index set has some value — there is no way to leave a cell
|
|
95
|
+
unassigned and still have a solution) and FUNCTIONAL (each cell holds
|
|
96
|
+
exactly one value), so it reaches the identical total-relation semantics the
|
|
97
|
+
ASP encoding reaches via an explicit boolean array plus an explicit
|
|
98
|
+
"exactly one true per input tuple" constraint, but through a solver-native
|
|
99
|
+
type rather than a constraint that would be redundant in this language —
|
|
100
|
+
exactly the reason CP is in this design at all (§11: "alldifferent for the
|
|
101
|
+
distinctness convention rather than the pairwise-≠ expansion"; the same
|
|
102
|
+
argument applies to functions, not just constants). A predicate ``p`` of
|
|
103
|
+
arity ``k`` is the boolean counterpart, ``array[DOM, …, DOM] of var bool:
|
|
104
|
+
p_name;``; a constant is ``var DOM: k_name;`` (the 0-ary case of the same
|
|
105
|
+
"exactly one value" reading, likewise needing no explicit "exactly one"
|
|
106
|
+
constraint because ``var DOM`` already ranges over exactly one value).
|
|
107
|
+
:func:`_atoms_from_solution` is what actually produces the ``(k+1)``-tuple
|
|
108
|
+
shape :func:`~unicode_logic_kit.atp.finite_domain.structure_from_solution`
|
|
109
|
+
requires, by pairing every ``DOM^k`` index tuple with the array's value
|
|
110
|
+
there — so the two modules' conventions meet at the atom boundary, not
|
|
111
|
+
inside the ``.mzn`` text itself.
|
|
112
|
+
|
|
113
|
+
Arithmetic function symbols (``+``, ``-``, ``*``, ``/``) are refused
|
|
114
|
+
------------------------------------------------------------------------
|
|
115
|
+
``+``/``-``/``*``/``/`` count as ``Function`` nodes, and
|
|
116
|
+
:func:`~unicode_logic_kit.atp.finite_domain.fragment_check` now ADMITS
|
|
117
|
+
``Function`` generally (see the module docstring's opening) — but these four
|
|
118
|
+
NAMES stay refused regardless, LIVE, on the exact path the rest of this
|
|
119
|
+
module's ``Function`` support now takes: :func:`_term`'s ``Function`` branch
|
|
120
|
+
checks ``node.name in _BUILTIN_ARITH_FUNCS`` before it ever consults
|
|
121
|
+
``ctx.functions``, so an arithmetic-operator sentence is stopped inside
|
|
122
|
+
:func:`to_minizinc`'s own rendering loop, ``NotImplementedError``, before a
|
|
123
|
+
single array is declared — no longer merely a second line of defence for a
|
|
124
|
+
hand-built problem :func:`fragment_check` never saw, now the FIRST and only
|
|
125
|
+
line of defence for these four names specifically (an ordinary declared
|
|
126
|
+
function ``f(x)`` no longer shares this fate — see "Encoding — arrays and
|
|
127
|
+
generators" above). This section states, and argues below, WHICH reading of
|
|
128
|
+
``+`` this module gives it now that ``Function`` support is no longer merely
|
|
129
|
+
hypothetical.
|
|
130
|
+
:meth:`~unicode_logic_kit.fol.nodes.Function.to_z3` treats ``+`` as an
|
|
131
|
+
UNINTERPRETED function symbol named ``"+"`` (``env.get_func(self.name,
|
|
132
|
+
len(self.args))`` — the same call for every function name; genuine
|
|
133
|
+
arithmetic only exists in the separate, unrelated
|
|
134
|
+
:mod:`~unicode_logic_kit.atp.z3_arith` pipeline), and
|
|
135
|
+
:meth:`~unicode_logic_kit.fol.signature.Signature.from_formulas` deliberately
|
|
136
|
+
EXCLUDES these four names from the ``functions`` section it builds (the
|
|
137
|
+
``_BUILTIN_FUNCS`` split, mirroring ``eval.validate``'s identical carve-out)
|
|
138
|
+
— so a :class:`~unicode_logic_kit.atp.finite_domain.FiniteDomainProblem`'s
|
|
139
|
+
auto-derived signature never declares an array for them. Two readings were
|
|
140
|
+
available here: (a) silently declare one anyway, duplicating a symbol the
|
|
141
|
+
signature layer deliberately keeps out of user vocabulary, or (b) give ``+``
|
|
142
|
+
literal MiniZinc integer-arithmetic semantics (tempting, since domain
|
|
143
|
+
individuals ARE the integers ``0 … n-1`` already) — but that would be a
|
|
144
|
+
DIFFERENT reading of the identical AST node than every other export in this
|
|
145
|
+
kit gives it, decided unilaterally inside one backend. Per the kit's own
|
|
146
|
+
discipline against exactly this kind of silent semantic substitution (see
|
|
147
|
+
e.g. :mod:`atp.finite_domain`'s ``all_different`` footnote), this module
|
|
148
|
+
instead REFUSES: :func:`_term` raises ``NotImplementedError`` for these four
|
|
149
|
+
names — reached LIVE now through :meth:`MinizincBackend.decide` whenever a
|
|
150
|
+
searched sentence names one of them, since :func:`fragment_check` no longer
|
|
151
|
+
stops a ``Function`` node on the way in; the caller sees the identical
|
|
152
|
+
``UNKNOWN``/``"unsupported"`` outcome either way, just raised one call
|
|
153
|
+
frame deeper than before, from inside :func:`to_minizinc`'s rendering loop
|
|
154
|
+
rather than at the gate — an honest gap either way, not a guess. (A
|
|
155
|
+
:class:`~unicode_logic_kit.fol.nodes.Number` is refused too, for its own reason —
|
|
156
|
+
see the next section.)
|
|
157
|
+
|
|
158
|
+
A ``Number`` is the bound of a count, never an individual
|
|
159
|
+
-----------------------------------------------------------
|
|
160
|
+
A numeral (:class:`~unicode_logic_kit.fol.nodes.Number`) has two readings in the
|
|
161
|
+
kit. On every route that was not asked for arithmetic it is a CONSTANT
|
|
162
|
+
identified by its value (``Number(1)`` and ``Number(1.0)`` are one node, one
|
|
163
|
+
constant; ``1 ≠ 2`` is not valid, because two constants may denote one
|
|
164
|
+
element), as :meth:`~unicode_logic_kit.fol.nodes.Number.to_z3` writes it. The
|
|
165
|
+
documented counting fragment gives it the other one: the number a cardinality
|
|
166
|
+
is compared with (``|{v : φ}| ≥ 3`` — a raw count, unrelated to any domain
|
|
167
|
+
individual). This backend's domain individuals are the integers ``0 … n-1``,
|
|
168
|
+
so the bare literal ``k`` would be a DOMAIN ELEMENT, the element number ``k``
|
|
169
|
+
(or no element at all when ``k`` is not below the size), and the kit's
|
|
170
|
+
numerals need not be that: ``(∀x ∀y x = y) → 1 = 2`` is valid (one element,
|
|
171
|
+
so ``1`` and ``2`` denote the same thing), and the index reading refuted it.
|
|
172
|
+
|
|
173
|
+
So a ``Number`` is read as a number ONLY as an operand of a comparison with a
|
|
174
|
+
:class:`~unicode_logic_kit.fol.nodes.Cardinality` (:func:`_number_int`: the
|
|
175
|
+
bound of the count, a float with a whole value being that integer). Anywhere
|
|
176
|
+
else — as a term of a predicate, of a function, of ``=``, or in a comparison
|
|
177
|
+
of numerals that has no cardinality in it — it is refused by name with
|
|
178
|
+
``NotImplementedError``, which :meth:`MinizincBackend.decide` reports as
|
|
179
|
+
``UNKNOWN``/``"unsupported"``: this backend has no symbol for a numeral, and
|
|
180
|
+
it never reads one as a domain index or as arithmetic. The same holds for a
|
|
181
|
+
comparison that sets a cardinality against a plain individual (the count
|
|
182
|
+
against the element number of a constant has no coherent reading). The
|
|
183
|
+
:class:`~unicode_logic_kit.atp.clingo_backend.ClingoBackend` draws the same line.
|
|
184
|
+
|
|
185
|
+
Identifier scheme — role-prefixed, ASCII-only, injective
|
|
186
|
+
---------------------------------------------------------
|
|
187
|
+
Every declared symbol is rendered under a role prefix — predicate ``p``,
|
|
188
|
+
function ``f``, constant ``k``, bound variable ``v`` — rather than its
|
|
189
|
+
bare kit-level name, for two independent reasons. First, MiniZinc reserves a
|
|
190
|
+
long list of lower-case keywords (``output``, ``function``, ``predicate``,
|
|
191
|
+
``array``, ``where``, ``let``, …), and while this kit's own grammar (see
|
|
192
|
+
``fol/grammars/terminals.lark``) happens to keep bound VARIABLE names to a
|
|
193
|
+
single letter plus digits (so they can never collide with a multi-letter
|
|
194
|
+
keyword), it places NO such restriction on a function/constant NAME token —
|
|
195
|
+
a formula naming a function ``output`` would otherwise silently break the
|
|
196
|
+
renderer's own ``output`` item. Prefixing removes the possibility outright
|
|
197
|
+
rather than relying on the grammar's current shape staying that way forever.
|
|
198
|
+
Second, it keeps predicates, functions and constants in three textually
|
|
199
|
+
disjoint MiniZinc namespaces, matching how the kit's own
|
|
200
|
+
:class:`~unicode_logic_kit.fol.signature.Signature` already keeps them apart
|
|
201
|
+
conceptually.
|
|
202
|
+
|
|
203
|
+
A MiniZinc identifier is an ASCII letter followed by ASCII letters, digits and
|
|
204
|
+
underscores, and is not a keyword. A kit-level name need not be one: it may be
|
|
205
|
+
non-ASCII (Greek, e.g. ``θ``, or any other script the widened FOL grammar
|
|
206
|
+
accepts, e.g. ``świątek``), and since a constant may be written in quotes, and a
|
|
207
|
+
TPTP file may quote a predicate or a function, it may hold a space, a hyphen, a
|
|
208
|
+
quote, a ``+``, or any other character (``'a b'``, ``'G-910'``, ``'C++'``). So a
|
|
209
|
+
name is written in one of two forms, and every identifier of a model is one of
|
|
210
|
+
them:
|
|
211
|
+
|
|
212
|
+
* **The plain form** ``<role>_<name>``, for a name that consists only of ASCII
|
|
213
|
+
letters, digits and underscores once the kit's own
|
|
214
|
+
:func:`~unicode_logic_kit.fol._fol_nodes.constant_name_to_ascii` has folded it
|
|
215
|
+
(the transliteration :meth:`~unicode_logic_kit.fol.nodes.Constant.to_prover9` /
|
|
216
|
+
``to_tptp`` already use: ``θ`` becomes ``theta``, another non-ASCII
|
|
217
|
+
character a ``uXXXX`` escape). It is the form this module always wrote, and it
|
|
218
|
+
stays exactly as it was for every name it was legal for, so a model that was
|
|
219
|
+
written before is written again byte for byte (the files under
|
|
220
|
+
``tests/fixtures/minizinc/``).
|
|
221
|
+
* **The escaped form** ``<role>x_<code>``, for every other name. ``<code>`` is
|
|
222
|
+
the name with each ASCII letter and digit kept and every other character
|
|
223
|
+
replaced by an underscore, the code point of the character in lower-case
|
|
224
|
+
hexadecimal, and an underscore: ``a b`` is ``kx_a_20_b``, ``G-910`` is
|
|
225
|
+
``kx_G_2d_910``, ``C++`` is ``kx_C_2b__2b_``.
|
|
226
|
+
|
|
227
|
+
The escaped form cannot meet any other identifier of a model, for three reasons.
|
|
228
|
+
(1) Every plain identifier has an underscore as its second character and every
|
|
229
|
+
escaped one has an ``x`` there (``k_a`` against ``kx_a``), so the two forms never
|
|
230
|
+
meet, in one role or across roles. (2) The model's own names are ``n``, ``DOM``
|
|
231
|
+
and the index variables ``i0``, ``i1``, … of the ``output`` item, none of which
|
|
232
|
+
holds an underscore, and no MiniZinc keyword holds one either, while an escaped
|
|
233
|
+
identifier always has one in its third position. (3) Within the escaped form the
|
|
234
|
+
code cannot be read two ways: a letter or digit stands for itself, and every
|
|
235
|
+
underscore opens exactly one ``_<hex>_`` group (an underscore of the name is
|
|
236
|
+
written ``_5f_``, so none appears bare), so two different names give two
|
|
237
|
+
different codes.
|
|
238
|
+
|
|
239
|
+
The plain form is NOT injective in general (a literal ``theta`` and the Greek
|
|
240
|
+
``θ`` both fold to ``theta``; a literal ``u03d1`` and the theta symbol ``ϑ``
|
|
241
|
+
fold alike), and it
|
|
242
|
+
cannot be changed for those names without changing the identifier of a name that
|
|
243
|
+
was legal. :func:`to_minizinc` therefore runs the exact same collision guard
|
|
244
|
+
:func:`~unicode_logic_kit.atp._tptp_problem.generate_tptp_problem` already runs
|
|
245
|
+
for its own (different) folding function, separately for the predicate,
|
|
246
|
+
function, constant and variable namespaces, refusing with
|
|
247
|
+
``NotImplementedError`` naming both colliding kit-level names rather than
|
|
248
|
+
silently merging two distinct symbols into one MiniZinc identifier. (The
|
|
249
|
+
variable namespace needs the guard for the same reason: two bound variables
|
|
250
|
+
that share an identifier would make an inner quantifier capture the outer one's
|
|
251
|
+
occurrences.)
|
|
252
|
+
|
|
253
|
+
Output — a hand-written wire format, not ``--output-mode json``
|
|
254
|
+
---------------------------------------------------------------------
|
|
255
|
+
MiniZinc 2.5+ can auto-serialise a solution to JSON
|
|
256
|
+
(``--output-mode json``), and that would ordinarily be the easy choice. It
|
|
257
|
+
is deliberately NOT used here: its exact behaviour when a model has no
|
|
258
|
+
explicit ``output`` item — which decision variables get included, and in
|
|
259
|
+
what shape a multi-dimensional array is emitted — is a MiniZinc-VERSION
|
|
260
|
+
detail this repository has no installed binary to confirm against (see the
|
|
261
|
+
top of this docstring). Instead :func:`to_minizinc` writes its OWN ``output``
|
|
262
|
+
item using only ``show()`` and string concatenation — base-language MiniZinc
|
|
263
|
+
features stable across every 2.x release — that flattens every array
|
|
264
|
+
(predicate or function, any arity) to a ONE-DIMENSIONAL list via an explicit
|
|
265
|
+
index-generator comprehension (``[ f_name[i0,i1] | i0 in DOM, i1 in DOM ]``,
|
|
266
|
+
varying the LAST generator fastest — the same nesting order
|
|
267
|
+
:func:`itertools.product` produces in Python, which is what
|
|
268
|
+
:func:`_atoms_from_solution` uses to reverse the flattening), and tags each
|
|
269
|
+
line ``UFK <mzn_name> <arity> <show(...)>`` between ``UFK-SOLUTION-BEGIN`` /
|
|
270
|
+
``UFK-SOLUTION-END`` sentinels. The whole format is invented, owned, and
|
|
271
|
+
parsed by this module alone — nothing about it depends on a MiniZinc version
|
|
272
|
+
or an installed solver, so it can be pinned by hand-written fixtures
|
|
273
|
+
(:func:`_parse_minizinc_solution`) and reasoned through line by line without
|
|
274
|
+
ever running ``minizinc``.
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
import itertools
|
|
278
|
+
import os
|
|
279
|
+
import re
|
|
280
|
+
import shutil
|
|
281
|
+
import subprocess
|
|
282
|
+
import tempfile
|
|
283
|
+
import time
|
|
284
|
+
from dataclasses import dataclass
|
|
285
|
+
from typing import Callable, Dict, Iterable, List, Optional, Sequence, Tuple, Union
|
|
286
|
+
|
|
287
|
+
from ..fol._fol_nodes import constant_name_to_ascii
|
|
288
|
+
from ..fol.nodes import (
|
|
289
|
+
Node, Variable, Constant, Number, Function,
|
|
290
|
+
Atom, Not, And, Or, Xor, Implies, Iff, Contrast, Quantifier,
|
|
291
|
+
Count, Cardinality,
|
|
292
|
+
)
|
|
293
|
+
from ..fol.signature import Signature
|
|
294
|
+
from ..fol._free_parameters import parameterize
|
|
295
|
+
from .finite_domain import (
|
|
296
|
+
FiniteDomainProblem, fragment_check, free_variable_reason, lower_msfol,
|
|
297
|
+
structure_from_solution, verify_model,
|
|
298
|
+
)
|
|
299
|
+
from .protocol import (
|
|
300
|
+
BackendUnavailable, ERROR, ProverBackend, REFUTED, UNKNOWN, Verdict,
|
|
301
|
+
_native_command_exists,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
__all__ = ["MinizincBackend", "minizinc_available", "to_minizinc"]
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# =============================================================================
|
|
308
|
+
# Binary discovery
|
|
309
|
+
# =============================================================================
|
|
310
|
+
|
|
311
|
+
def _minizinc_binary() -> Optional[str]:
|
|
312
|
+
"""Discover the ``minizinc`` CLI: ``$UFK_MINIZINC``, then ``PATH``.
|
|
313
|
+
|
|
314
|
+
Mirrors :meth:`~unicode_logic_kit.atp.protocol.Prover9Backend._binary` /
|
|
315
|
+
:meth:`~unicode_logic_kit.atp.protocol.VampireBackend._binary` exactly:
|
|
316
|
+
the env var is read fresh on every call (never cached, so repointing it
|
|
317
|
+
mid-process takes effect immediately) and there is no WSL bridging —
|
|
318
|
+
unlike :mod:`atp.eprover_backend`'s Linux-only tools, MiniZinc ships
|
|
319
|
+
native Windows/macOS/Linux installers, so there is no "only reachable
|
|
320
|
+
inside WSL" case to handle here.
|
|
321
|
+
"""
|
|
322
|
+
return os.environ.get("UFK_MINIZINC") or shutil.which("minizinc")
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def minizinc_available() -> bool:
|
|
326
|
+
"""Pure discovery: is a ``minizinc`` binary reachable right now?
|
|
327
|
+
|
|
328
|
+
Checks ``$UFK_MINIZINC`` then ``PATH`` (see :func:`_minizinc_binary`);
|
|
329
|
+
never imports or spawns anything, so it is safe to call speculatively.
|
|
330
|
+
"""
|
|
331
|
+
return _minizinc_binary() is not None
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
# =============================================================================
|
|
335
|
+
# Identifier scheme
|
|
336
|
+
# =============================================================================
|
|
337
|
+
|
|
338
|
+
# What may follow the role prefix of a plain identifier: ASCII letters, digits and
|
|
339
|
+
# underscores, written out as ranges so that a Unicode letter or digit never matches.
|
|
340
|
+
_MZN_PLAIN_TAIL = re.compile(r"[A-Za-z0-9_]*")
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _mzn_escape(name: str) -> str:
|
|
344
|
+
"""The code of ``name`` in an escaped identifier: ASCII letters and digits stay, any other
|
|
345
|
+
character becomes an underscore, its code point in lower-case hexadecimal, an underscore.
|
|
346
|
+
|
|
347
|
+
Injective: a letter or digit stands for itself, and every underscore of the result
|
|
348
|
+
opens exactly one ``_<hex>_`` group (an underscore of ``name`` is the group ``_5f_``),
|
|
349
|
+
so the result reads back one way only. The result holds only ASCII letters, digits and
|
|
350
|
+
underscores. See the module docstring's "Identifier scheme" section.
|
|
351
|
+
"""
|
|
352
|
+
return "".join(
|
|
353
|
+
ch if ch.isascii() and ch.isalnum() else f"_{ord(ch):x}_"
|
|
354
|
+
for ch in name
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _mzn_identifier(role: str, name: str, kind: str) -> str:
|
|
359
|
+
"""The MiniZinc identifier of the ``kind`` symbol ``name``, written under the one-letter ``role``.
|
|
360
|
+
|
|
361
|
+
The plain form ``<role>_<name>`` when the name, folded by
|
|
362
|
+
:func:`~unicode_logic_kit.fol._fol_nodes.constant_name_to_ascii`, is made of ASCII letters,
|
|
363
|
+
digits and underscores only (the form this module always wrote, so such a name keeps its
|
|
364
|
+
identifier); the escaped form ``<role>x_<code>`` for every other name (see
|
|
365
|
+
:func:`_mzn_escape`). The two forms never meet: a plain identifier has an underscore as
|
|
366
|
+
its second character, an escaped one an ``x``. See the module docstring's "Identifier
|
|
367
|
+
scheme" section for that argument and for the one case this does not settle (two names
|
|
368
|
+
that fold to one plain identifier), which :func:`_mzn_names_or_raise` refuses.
|
|
369
|
+
|
|
370
|
+
Raises:
|
|
371
|
+
NotImplementedError: ``name`` is not a string, so it has no identifier.
|
|
372
|
+
"""
|
|
373
|
+
if not isinstance(name, str):
|
|
374
|
+
raise NotImplementedError(
|
|
375
|
+
f"to_minizinc: the {kind} name {name!r} is of type {type(name).__name__}, not a "
|
|
376
|
+
"string, so it has no MiniZinc identifier."
|
|
377
|
+
)
|
|
378
|
+
folded = constant_name_to_ascii(name)
|
|
379
|
+
if _MZN_PLAIN_TAIL.fullmatch(folded):
|
|
380
|
+
return f"{role}_{folded}"
|
|
381
|
+
return f"{role}x_{_mzn_escape(name)}"
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _mzn_pred_name(name: str) -> str:
|
|
385
|
+
"""Predicate ``name`` -> its MiniZinc identifier, ``p_<name>`` or the escaped ``px_<code>``.
|
|
386
|
+
|
|
387
|
+
A predicate NAME token is not restricted to ASCII (see ``fol/grammars/terminals.lark``:
|
|
388
|
+
PREDICATE admits any Unicode letter with ``str.isupper()``), and a TPTP file may quote
|
|
389
|
+
one that holds a space or a hyphen; MiniZinc identifiers are ASCII letters, digits and
|
|
390
|
+
underscores, so the name goes through :func:`_mzn_identifier`. This function does not
|
|
391
|
+
check for two names that share an identifier (it has no visibility into sibling
|
|
392
|
+
predicates); :func:`to_minizinc` does.
|
|
393
|
+
"""
|
|
394
|
+
return _mzn_identifier("p", name, "predicate")
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _mzn_func_name(name: str) -> str:
|
|
398
|
+
"""Function ``name`` -> its MiniZinc identifier, ``f_<name>`` or the escaped ``fx_<code>``.
|
|
399
|
+
|
|
400
|
+
Same reasoning and the same collision caveat as :func:`_mzn_pred_name` — see
|
|
401
|
+
the module docstring's "Identifier scheme" section.
|
|
402
|
+
"""
|
|
403
|
+
return _mzn_identifier("f", name, "function")
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _mzn_const_name(name: str) -> str:
|
|
407
|
+
"""Constant ``name`` -> its MiniZinc identifier, ``k_<name>`` or the escaped ``kx_<code>``.
|
|
408
|
+
|
|
409
|
+
A constant may be written in quotes, so its name may be any text (``'a b'``,
|
|
410
|
+
``'G-910'``, ``'C++'``); see :func:`_mzn_identifier` and the module docstring's
|
|
411
|
+
"Identifier scheme" section. Two names that fold to one plain identifier (``theta`` and
|
|
412
|
+
``θ``) are refused by :func:`to_minizinc`, which sees all the constants at once; this
|
|
413
|
+
function does not.
|
|
414
|
+
"""
|
|
415
|
+
return _mzn_identifier("k", name, "constant")
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _mzn_var_name(name: str) -> str:
|
|
419
|
+
"""Bound variable ``name`` -> its MiniZinc identifier, ``v_<name>`` or the escaped ``vx_<code>``.
|
|
420
|
+
|
|
421
|
+
The VARIABLE terminal (``fol/grammars/terminals.lark``) admits any Unicode letter, so a
|
|
422
|
+
raw non-ASCII variable name (e.g. ``ą``) would otherwise reach the generated ``.mzn``
|
|
423
|
+
text verbatim — not a legal MiniZinc identifier — and a hand-built variable may be named
|
|
424
|
+
anything. A bound variable's identifier is used only inside the ``forall``, ``exists`` or
|
|
425
|
+
comprehension generator that binds it, but two distinct variable names that fold to one
|
|
426
|
+
identifier would let an inner binder capture the outer one's occurrences, so
|
|
427
|
+
:func:`to_minizinc` runs the collision guard over the variable names too.
|
|
428
|
+
"""
|
|
429
|
+
return _mzn_identifier("v", name, "variable")
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
# The four arithmetic operators are legal Function names, and fragment_check
|
|
433
|
+
# now admits Function generally (see the module docstring's opening) — but
|
|
434
|
+
# these four names are excluded from Signature.from_formulas's `functions`
|
|
435
|
+
# section (the `_BUILTIN_FUNCS` split) regardless, so _term checks this set
|
|
436
|
+
# BEFORE consulting ctx.functions and refuses them LIVE, reachable through
|
|
437
|
+
# decide() on an ordinary search now (see the module docstring's "Arithmetic
|
|
438
|
+
# function symbols" section) — this module still refuses to guess a reading
|
|
439
|
+
# for them on its own rather than pick one silently.
|
|
440
|
+
_BUILTIN_ARITH_FUNCS = frozenset({"+", "-", "*", "/"})
|
|
441
|
+
|
|
442
|
+
# The six built-in comparison predicates: never declared in a Signature
|
|
443
|
+
# (mirroring `_BUILTIN_PREDS` in fol.signature / eval.validate), always
|
|
444
|
+
# rendered as a direct MiniZinc infix comparison instead of an array lookup.
|
|
445
|
+
_COMPARISON_OPS: Dict[str, str] = {
|
|
446
|
+
"=": "=", "≠": "!=", "<": "<", ">": ">", "≤": "<=", "≥": ">=",
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
_COUNT_OP_TO_MZN = {"ge": ">=", "le": "<=", "eq": "="}
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _number_int(node: Number) -> int:
|
|
453
|
+
"""Return the value of ``node``, the bound a cardinality is compared with, as a plain ``int``.
|
|
454
|
+
|
|
455
|
+
Only :func:`_formula`'s comparison branch calls this, for the ``Number`` operand of a
|
|
456
|
+
comparison with a :class:`~unicode_logic_kit.fol.nodes.Cardinality` (see the module
|
|
457
|
+
docstring's "A ``Number`` is the bound of a count, never an individual" section). A
|
|
458
|
+
:class:`~unicode_logic_kit.fol.nodes.Number` stores a float with a whole value as the
|
|
459
|
+
integer it equals, so ``2.0`` is the bound ``2``; a count cannot equal ``2.5``, so a
|
|
460
|
+
fractional ``Number`` — legal per the dataclass's own ``Union[int, float]`` type,
|
|
461
|
+
unlike :class:`~unicode_logic_kit.fol.nodes.Count`'s own ``n`` field — has no reading
|
|
462
|
+
and is refused rather than silently truncated.
|
|
463
|
+
|
|
464
|
+
Raises:
|
|
465
|
+
NotImplementedError: ``node.value`` is not a plain (non-bool) ``int``.
|
|
466
|
+
"""
|
|
467
|
+
if isinstance(node.value, bool) or not isinstance(node.value, int):
|
|
468
|
+
raise NotImplementedError(
|
|
469
|
+
f"to_minizinc: Number({node.value!r}) is not an integer — a count |{{v : φ}}| "
|
|
470
|
+
"is never equal to a fraction, so a comparison bound that is not an integer "
|
|
471
|
+
"has no reading in a finite domain of individuals 0..n-1."
|
|
472
|
+
)
|
|
473
|
+
return node.value
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
# =============================================================================
|
|
477
|
+
# Rendering context
|
|
478
|
+
# =============================================================================
|
|
479
|
+
|
|
480
|
+
@dataclass(frozen=True)
|
|
481
|
+
class _Ctx:
|
|
482
|
+
"""The name maps a single :func:`to_minizinc` call threads through rendering.
|
|
483
|
+
|
|
484
|
+
Built once per call from ``problem.signature`` (see :func:`to_minizinc`)
|
|
485
|
+
so :func:`_formula` / :func:`_term` never recompute a prefix or re-run
|
|
486
|
+
the constant collision check per node — every kit-level symbol name that
|
|
487
|
+
can legally appear in ``problem.sentences`` (per
|
|
488
|
+
:func:`~unicode_logic_kit.atp.finite_domain.fragment_check`, already
|
|
489
|
+
consulted by :func:`to_minizinc` before this is built) has exactly one
|
|
490
|
+
entry here.
|
|
491
|
+
"""
|
|
492
|
+
|
|
493
|
+
predicates: Dict[str, str]
|
|
494
|
+
functions: Dict[str, str]
|
|
495
|
+
constants: Dict[str, str]
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _term(node: Node, ctx: _Ctx) -> str:
|
|
499
|
+
"""Render ``node`` as a MiniZinc ``int``-typed expression (a TERM position).
|
|
500
|
+
|
|
501
|
+
Dispatches on node type; every branch either returns a self-contained
|
|
502
|
+
expression string or raises ``NotImplementedError`` naming exactly why
|
|
503
|
+
(an arithmetic function symbol, a ``Number`` used as an individual, an
|
|
504
|
+
undeclared symbol, or a node type with no term-position reading at all — the last
|
|
505
|
+
should be unreachable for a ``sentences`` tuple that already passed
|
|
506
|
+
:func:`~unicode_logic_kit.atp.finite_domain.fragment_check`, but is
|
|
507
|
+
checked explicitly rather than assumed).
|
|
508
|
+
"""
|
|
509
|
+
if isinstance(node, Variable):
|
|
510
|
+
return _mzn_var_name(node.name)
|
|
511
|
+
if isinstance(node, Constant):
|
|
512
|
+
mzn = ctx.constants.get(node.name)
|
|
513
|
+
if mzn is None:
|
|
514
|
+
raise NotImplementedError(
|
|
515
|
+
f"to_minizinc: constant {node.name!r} is not declared in the "
|
|
516
|
+
"problem's signature."
|
|
517
|
+
)
|
|
518
|
+
return mzn
|
|
519
|
+
if isinstance(node, Number):
|
|
520
|
+
raise NotImplementedError(
|
|
521
|
+
f"to_minizinc: the numeral {node.value!r} is used as an individual. A numeral "
|
|
522
|
+
"is a constant identified by its value (1 and 1.0 are one constant), and this "
|
|
523
|
+
"backend has no symbol for it: it reads a number only as the bound a count "
|
|
524
|
+
"|{v : φ}| is compared with, never as a domain element, because the index "
|
|
525
|
+
"reading would make 1 and 2 two different elements that the kit's numerals need "
|
|
526
|
+
"not be. Use a solver that reads numerals as constants (z3, the finite model "
|
|
527
|
+
"finder), or name the individual with a constant."
|
|
528
|
+
)
|
|
529
|
+
if isinstance(node, Function):
|
|
530
|
+
if node.name in _BUILTIN_ARITH_FUNCS:
|
|
531
|
+
raise NotImplementedError(
|
|
532
|
+
f"to_minizinc: the arithmetic function symbol {node.name!r} "
|
|
533
|
+
"carries no declared arity/extension here — "
|
|
534
|
+
"Signature.from_formulas excludes it as a built-in, and this "
|
|
535
|
+
"backend refuses to guess between an uninterpreted-function "
|
|
536
|
+
"reading and a literal-arithmetic one rather than picking "
|
|
537
|
+
"either silently; see the module docstring."
|
|
538
|
+
)
|
|
539
|
+
mzn = ctx.functions.get(node.name)
|
|
540
|
+
if mzn is None:
|
|
541
|
+
raise NotImplementedError(
|
|
542
|
+
f"to_minizinc: function {node.name!r} is not declared in the "
|
|
543
|
+
"problem's signature."
|
|
544
|
+
)
|
|
545
|
+
if not node.args:
|
|
546
|
+
return mzn
|
|
547
|
+
args = ", ".join(_term(a, ctx) for a in node.args)
|
|
548
|
+
return f"{mzn}[{args}]"
|
|
549
|
+
if isinstance(node, Cardinality):
|
|
550
|
+
var = _mzn_var_name(node.variable.name)
|
|
551
|
+
body = _formula(node.formula, ctx)
|
|
552
|
+
return f"sum({var} in DOM)(bool2int({body}))"
|
|
553
|
+
raise NotImplementedError(
|
|
554
|
+
f"to_minizinc: {type(node).__name__} has no term-position rendering."
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _comparison_operands(atom: Atom, ctx: _Ctx) -> Tuple[str, str]:
|
|
559
|
+
"""Render the two operands of the comparison ``atom`` (``= ≠ < > ≤ ≥`` at arity 2).
|
|
560
|
+
|
|
561
|
+
A comparison with a :class:`~unicode_logic_kit.fol.nodes.Cardinality` operand is a
|
|
562
|
+
counting comparison: both operands are then counting terms, a ``Cardinality`` (its
|
|
563
|
+
``sum`` over the domain) or a ``Number`` (the bound, :func:`_number_int`). Any other
|
|
564
|
+
comparison compares individuals, and a ``Number`` among them is refused by
|
|
565
|
+
:func:`_term`.
|
|
566
|
+
|
|
567
|
+
Raises:
|
|
568
|
+
NotImplementedError: a numeral is compared with no cardinality in the comparison
|
|
569
|
+
(a statement about constants, not about counts: ``1 = 2`` is not valid, and
|
|
570
|
+
``(∀x ∀y x = y) → 1 = 2`` is); a cardinality is compared with a plain
|
|
571
|
+
individual (no coherent reading); or an operand is refused by :func:`_term` /
|
|
572
|
+
:func:`_number_int`.
|
|
573
|
+
"""
|
|
574
|
+
has_cardinality = any(isinstance(a, Cardinality) for a in atom.args)
|
|
575
|
+
has_numeral = any(isinstance(a, Number) for a in atom.args)
|
|
576
|
+
if not has_cardinality and not has_numeral:
|
|
577
|
+
return _term(atom.args[0], ctx), _term(atom.args[1], ctx)
|
|
578
|
+
if not has_cardinality:
|
|
579
|
+
raise NotImplementedError(
|
|
580
|
+
f"to_minizinc: {atom.predicate!r} compares a numeral without a cardinality. A numeral "
|
|
581
|
+
"is a constant identified by its value, and nothing else is known about it, so such "
|
|
582
|
+
"a comparison is a statement about constants ('1 ≠ 2' is not valid, "
|
|
583
|
+
"'(∀x ∀y x = y) → 1 = 2' is), which this backend does not state: it reads a number "
|
|
584
|
+
"only as the bound a count |{v : φ}| is compared with, and a refutation found under "
|
|
585
|
+
"the index reading would be wrong for the constants. Use a solver that reads numerals "
|
|
586
|
+
"as constants (z3, the finite model finder), or compare a cardinality."
|
|
587
|
+
)
|
|
588
|
+
if not all(isinstance(a, (Cardinality, Number)) for a in atom.args):
|
|
589
|
+
raise NotImplementedError(
|
|
590
|
+
f"to_minizinc: {atom.predicate!r} compares a cardinality against a plain "
|
|
591
|
+
"individual-denoting term — both sides of a counting comparison must themselves be "
|
|
592
|
+
"counting terms (a Cardinality or a Number)."
|
|
593
|
+
)
|
|
594
|
+
left, right = (str(_number_int(a)) if isinstance(a, Number) else _term(a, ctx) for a in atom.args)
|
|
595
|
+
return left, right
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _formula(node: Node, ctx: _Ctx) -> str:
|
|
599
|
+
"""Render ``node`` as a MiniZinc ``bool``-typed expression (a FORMULA position).
|
|
600
|
+
|
|
601
|
+
Mirrors :func:`_term`'s dispatch/refusal discipline; see that function's
|
|
602
|
+
docstring for the shared conventions (undeclared symbols, unreachable
|
|
603
|
+
node types).
|
|
604
|
+
"""
|
|
605
|
+
if isinstance(node, Atom):
|
|
606
|
+
if node.predicate in _COMPARISON_OPS:
|
|
607
|
+
if len(node.args) != 2:
|
|
608
|
+
raise NotImplementedError(
|
|
609
|
+
f"to_minizinc: comparison predicate {node.predicate!r} "
|
|
610
|
+
f"used with arity {len(node.args)}, expected 2."
|
|
611
|
+
)
|
|
612
|
+
left, right = _comparison_operands(node, ctx)
|
|
613
|
+
return f"({left} {_COMPARISON_OPS[node.predicate]} {right})"
|
|
614
|
+
mzn = ctx.predicates.get(node.predicate)
|
|
615
|
+
if mzn is None:
|
|
616
|
+
raise NotImplementedError(
|
|
617
|
+
f"to_minizinc: predicate {node.predicate!r} is not declared "
|
|
618
|
+
"in the problem's signature."
|
|
619
|
+
)
|
|
620
|
+
if not node.args:
|
|
621
|
+
return mzn
|
|
622
|
+
args = ", ".join(_term(a, ctx) for a in node.args)
|
|
623
|
+
return f"{mzn}[{args}]"
|
|
624
|
+
if isinstance(node, Not):
|
|
625
|
+
return f"(not {_formula(node.formula, ctx)})"
|
|
626
|
+
if isinstance(node, And):
|
|
627
|
+
return f"({_formula(node.left, ctx)} /\\ {_formula(node.right, ctx)})"
|
|
628
|
+
if isinstance(node, Or):
|
|
629
|
+
return f"({_formula(node.left, ctx)} \\/ {_formula(node.right, ctx)})"
|
|
630
|
+
if isinstance(node, Xor):
|
|
631
|
+
return f"({_formula(node.left, ctx)} xor {_formula(node.right, ctx)})"
|
|
632
|
+
if isinstance(node, Implies):
|
|
633
|
+
return f"({_formula(node.left, ctx)} -> {_formula(node.right, ctx)})"
|
|
634
|
+
if isinstance(node, Iff):
|
|
635
|
+
return f"({_formula(node.left, ctx)} <-> {_formula(node.right, ctx)})"
|
|
636
|
+
if isinstance(node, Contrast):
|
|
637
|
+
# Truth-functionally And — see Contrast's own docstring in _fol_nodes.py.
|
|
638
|
+
return f"({_formula(node.left, ctx)} /\\ {_formula(node.right, ctx)})"
|
|
639
|
+
if isinstance(node, Quantifier):
|
|
640
|
+
var = _mzn_var_name(node.variable.name)
|
|
641
|
+
body = _formula(node.formula, ctx)
|
|
642
|
+
if node.type in ("forall", "∀"):
|
|
643
|
+
return f"forall({var} in DOM)({body})"
|
|
644
|
+
if node.type in ("exists", "∃"):
|
|
645
|
+
return f"exists({var} in DOM)({body})"
|
|
646
|
+
raise NotImplementedError(f"to_minizinc: unknown quantifier type {node.type!r}.")
|
|
647
|
+
if isinstance(node, Count):
|
|
648
|
+
var = _mzn_var_name(node.variable.name)
|
|
649
|
+
body = _formula(node.formula, ctx)
|
|
650
|
+
count_expr = f"sum({var} in DOM)(bool2int({body}))"
|
|
651
|
+
return f"({count_expr} {_COUNT_OP_TO_MZN[node.op]} {node.n.value})"
|
|
652
|
+
raise NotImplementedError(
|
|
653
|
+
f"to_minizinc: {type(node).__name__} has no formula-position "
|
|
654
|
+
"rendering (fragment_check should have refused this before "
|
|
655
|
+
"to_minizinc was reached)."
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
# =============================================================================
|
|
660
|
+
# to_minizinc — the renderer
|
|
661
|
+
# =============================================================================
|
|
662
|
+
|
|
663
|
+
def _flatten_expr(mzn_name: str, arity: int) -> str:
|
|
664
|
+
"""Return a MiniZinc expression flattening ``mzn_name`` to one dimension.
|
|
665
|
+
|
|
666
|
+
Arity 0 is the bare variable itself; arity >= 1 is an explicit
|
|
667
|
+
index-generator comprehension ``[ mzn_name[i0, i1, …] | i0 in DOM, i1 in
|
|
668
|
+
DOM, … ]`` that varies the LAST generator fastest — the same order
|
|
669
|
+
:func:`itertools.product` iterates in Python, which is what
|
|
670
|
+
:func:`_atoms_from_solution` relies on to invert this flattening. See the
|
|
671
|
+
module docstring's "Output" section for why the ``output`` item is
|
|
672
|
+
written this way instead of via ``--output-mode json``.
|
|
673
|
+
"""
|
|
674
|
+
if arity == 0:
|
|
675
|
+
return f"show({mzn_name})"
|
|
676
|
+
idx_vars = [f"i{k}" for k in range(arity)]
|
|
677
|
+
generators = ", ".join(f"{v} in DOM" for v in idx_vars)
|
|
678
|
+
index = ", ".join(idx_vars)
|
|
679
|
+
return f"show([{mzn_name}[{index}] | {generators}])"
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def _mzn_names_or_raise(
|
|
683
|
+
names: Iterable[str], namer: Callable[[str], str], kind: str,
|
|
684
|
+
) -> Dict[str, str]:
|
|
685
|
+
"""Map each raw ``name`` in ``names`` to ``namer(name)``, refusing a same-namespace
|
|
686
|
+
collision (two distinct kit-level ``kind`` symbols that get the same MiniZinc identifier).
|
|
687
|
+
|
|
688
|
+
Shared by :func:`to_minizinc`'s predicate, function, constant and variable
|
|
689
|
+
namespaces. The only names that can collide are two that fold to one plain
|
|
690
|
+
identifier (``theta`` and ``θ``; see the module docstring's "Identifier scheme"
|
|
691
|
+
section): an escaped identifier is different for every name and never equals a
|
|
692
|
+
plain one.
|
|
693
|
+
"""
|
|
694
|
+
out: Dict[str, str] = {}
|
|
695
|
+
seen: Dict[str, str] = {}
|
|
696
|
+
for name in sorted(names):
|
|
697
|
+
mzn = namer(name)
|
|
698
|
+
prior = seen.get(mzn)
|
|
699
|
+
if prior is not None and prior != name:
|
|
700
|
+
# NotImplementedError, not ValueError: this mirrors
|
|
701
|
+
# atp._tptp_problem.generate_tptp_problem's IDENTICAL collision
|
|
702
|
+
# guard for the (differently-folded) TPTP export, which raises
|
|
703
|
+
# the same type for the same reason -- an export whose folding
|
|
704
|
+
# is not injective for this particular pair of names is an
|
|
705
|
+
# EXPORT LIMITATION, not a malformed FiniteDomainProblem, and
|
|
706
|
+
# using the same exception type lets MinizincBackend.decide's
|
|
707
|
+
# existing "fragment this backend cannot encode" catch site
|
|
708
|
+
# handle it without a second, redundant except clause.
|
|
709
|
+
raise NotImplementedError(
|
|
710
|
+
f"to_minizinc: distinct {kind}s {prior!r} and {name!r} "
|
|
711
|
+
f"would both render as the MiniZinc identifier {mzn!r} "
|
|
712
|
+
"(the ASCII folding of a name, constant_name_to_ascii, is not injective) — refusing to "
|
|
713
|
+
"silently merge two distinct symbols; rename one of them "
|
|
714
|
+
"before exporting this problem. Mirrors "
|
|
715
|
+
"atp._tptp_problem.generate_tptp_problem's identical "
|
|
716
|
+
"collision guard for the TPTP export."
|
|
717
|
+
)
|
|
718
|
+
seen[mzn] = name
|
|
719
|
+
out[name] = mzn
|
|
720
|
+
return out
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def _bound_variable_names(sentences: Iterable[Node]) -> List[str]:
|
|
724
|
+
"""Every variable name that ``sentences`` write: occurrences, and the variable a
|
|
725
|
+
quantifier, a count or a cardinality binds (a binder is written even when its body
|
|
726
|
+
does not use the variable)."""
|
|
727
|
+
names = set()
|
|
728
|
+
for sentence in sentences:
|
|
729
|
+
for node in sentence.walk():
|
|
730
|
+
if isinstance(node, Variable):
|
|
731
|
+
names.add(node.name)
|
|
732
|
+
elif isinstance(node, (Quantifier, Count, Cardinality)):
|
|
733
|
+
names.add(node.variable.name)
|
|
734
|
+
return sorted(names)
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def to_minizinc(problem: FiniteDomainProblem) -> str:
|
|
738
|
+
"""Render ``problem`` as a complete MiniZinc model — the text of an ``mzn`` file.
|
|
739
|
+
|
|
740
|
+
The model declares ``DOM = 0..size-1``, one array (predicate/function) or
|
|
741
|
+
scalar (constant) per declared symbol in ``problem.signature``, an
|
|
742
|
+
``alldifferent`` constraint over the constants iff ``problem.all_different``
|
|
743
|
+
and at least one constant is declared, one ``constraint`` per sentence in
|
|
744
|
+
``problem.sentences``, ``solve satisfy;``, and a custom ``output`` item
|
|
745
|
+
(see the module docstring's "Output" section) that
|
|
746
|
+
:func:`_parse_minizinc_solution` / :func:`_atoms_from_solution` read back
|
|
747
|
+
on the other side of a solver run. Deterministic: symbols are declared
|
|
748
|
+
in sorted-name order within each section, so two calls on
|
|
749
|
+
structurally-equal problems byte-for-byte agree — the property a
|
|
750
|
+
checked-in fixture comparison in a test suite relies on.
|
|
751
|
+
|
|
752
|
+
Args:
|
|
753
|
+
problem: the search problem; ``problem.sentences`` must already pass
|
|
754
|
+
:func:`~unicode_logic_kit.atp.finite_domain.fragment_check` (this
|
|
755
|
+
function calls it itself, first, so a caller does not have to
|
|
756
|
+
remember to).
|
|
757
|
+
|
|
758
|
+
Returns:
|
|
759
|
+
The model text, newline-terminated.
|
|
760
|
+
|
|
761
|
+
Raises:
|
|
762
|
+
TypeError: ``problem`` is not a
|
|
763
|
+
:class:`~unicode_logic_kit.atp.finite_domain.FiniteDomainProblem`.
|
|
764
|
+
NotImplementedError: ``problem.sentences`` fails
|
|
765
|
+
:func:`~unicode_logic_kit.atp.finite_domain.fragment_check`; a
|
|
766
|
+
sentence has a free variable
|
|
767
|
+
(:func:`~unicode_logic_kit.atp.finite_domain.free_variable_reason`;
|
|
768
|
+
:meth:`MinizincBackend.decide` replaces it by a parameter before
|
|
769
|
+
it writes); a
|
|
770
|
+
sentence uses a node this renderer itself cannot place (an
|
|
771
|
+
arithmetic function symbol, a
|
|
772
|
+
:class:`~unicode_logic_kit.fol.nodes.Number` that is not the bound of a
|
|
773
|
+
comparison with a cardinality or is not an integer, or a symbol absent
|
|
774
|
+
from ``problem.signature`` — see :func:`_term` / :func:`_formula`);
|
|
775
|
+
a symbol name is not a string; or two distinct predicates,
|
|
776
|
+
functions, constants or bound variables would get the same
|
|
777
|
+
MiniZinc identifier (two names that fold to one plain identifier,
|
|
778
|
+
``theta`` and ``θ``; see the module docstring's "Identifier
|
|
779
|
+
scheme" section — raised as ``NotImplementedError``, not
|
|
780
|
+
``ValueError``, to match
|
|
781
|
+
:func:`~unicode_logic_kit.atp._tptp_problem.generate_tptp_problem`'s
|
|
782
|
+
identical collision guard and so it is caught by the same
|
|
783
|
+
"cannot encode this fragment" site in
|
|
784
|
+
:meth:`MinizincBackend.decide`). A name that is no MiniZinc
|
|
785
|
+
identifier by itself (a quoted constant ``'a b'``) is not a
|
|
786
|
+
reason to raise: it is written in its escaped form.
|
|
787
|
+
"""
|
|
788
|
+
if not isinstance(problem, FiniteDomainProblem):
|
|
789
|
+
raise TypeError(
|
|
790
|
+
f"to_minizinc: expected a FiniteDomainProblem, got "
|
|
791
|
+
f"{type(problem).__name__}."
|
|
792
|
+
)
|
|
793
|
+
reason = fragment_check(problem.sentences)
|
|
794
|
+
if reason is None:
|
|
795
|
+
# A free variable would be written as an identifier the model never declares.
|
|
796
|
+
reason = free_variable_reason(problem.sentences)
|
|
797
|
+
if reason is not None:
|
|
798
|
+
raise NotImplementedError(f"to_minizinc: {reason}")
|
|
799
|
+
|
|
800
|
+
sig = problem.signature
|
|
801
|
+
|
|
802
|
+
pred_names = _mzn_names_or_raise(sig.predicates, _mzn_pred_name, "predicate")
|
|
803
|
+
func_names = _mzn_names_or_raise(sig.functions, _mzn_func_name, "function")
|
|
804
|
+
const_names = _mzn_names_or_raise(sig.constants, _mzn_const_name, "constant")
|
|
805
|
+
# The identifier of a bound variable is not stored (see _term), so only the guard runs.
|
|
806
|
+
_mzn_names_or_raise(_bound_variable_names(problem.sentences), _mzn_var_name, "variable")
|
|
807
|
+
|
|
808
|
+
needs_alldifferent = bool(problem.all_different and const_names)
|
|
809
|
+
|
|
810
|
+
lines: List[str] = []
|
|
811
|
+
if needs_alldifferent:
|
|
812
|
+
lines.append('include "alldifferent.mzn";')
|
|
813
|
+
lines.append("")
|
|
814
|
+
lines.append(
|
|
815
|
+
"% Generated by unicode_logic_kit.atp.minizinc_backend.to_minizinc --"
|
|
816
|
+
)
|
|
817
|
+
lines.append(
|
|
818
|
+
f"% bounded refutation search, |D| = {problem.size}, "
|
|
819
|
+
f"{len(problem.sentences)} sentence(s) to satisfy simultaneously."
|
|
820
|
+
)
|
|
821
|
+
lines.append(
|
|
822
|
+
"% REFUTATION-ONLY: SATISFIABLE here witnesses a finite countermodel;"
|
|
823
|
+
)
|
|
824
|
+
lines.append(
|
|
825
|
+
"% UNSATISFIABLE proves nothing about validity at a larger domain size."
|
|
826
|
+
)
|
|
827
|
+
lines.append("")
|
|
828
|
+
lines.append(f"int: n = {problem.size};")
|
|
829
|
+
lines.append("set of int: DOM = 0..n-1;")
|
|
830
|
+
lines.append("")
|
|
831
|
+
|
|
832
|
+
for name in sorted(sig.predicates):
|
|
833
|
+
decl = sig.predicates[name]
|
|
834
|
+
mzn = pred_names[name]
|
|
835
|
+
if decl.arity == 0:
|
|
836
|
+
lines.append(f"var bool: {mzn};")
|
|
837
|
+
else:
|
|
838
|
+
dims = ", ".join(["DOM"] * decl.arity)
|
|
839
|
+
lines.append(f"array[{dims}] of var bool: {mzn};")
|
|
840
|
+
if sig.predicates:
|
|
841
|
+
lines.append("")
|
|
842
|
+
|
|
843
|
+
for name in sorted(sig.functions):
|
|
844
|
+
decl = sig.functions[name]
|
|
845
|
+
mzn = func_names[name]
|
|
846
|
+
if decl.arity == 0:
|
|
847
|
+
lines.append(f"var DOM: {mzn};")
|
|
848
|
+
else:
|
|
849
|
+
dims = ", ".join(["DOM"] * decl.arity)
|
|
850
|
+
lines.append(f"array[{dims}] of var DOM: {mzn};")
|
|
851
|
+
if sig.functions:
|
|
852
|
+
lines.append("")
|
|
853
|
+
|
|
854
|
+
for name in sorted(const_names):
|
|
855
|
+
lines.append(f"var DOM: {const_names[name]};")
|
|
856
|
+
if const_names:
|
|
857
|
+
lines.append("")
|
|
858
|
+
|
|
859
|
+
if needs_alldifferent:
|
|
860
|
+
names = [const_names[n] for n in sorted(const_names)]
|
|
861
|
+
lines.append(f"constraint alldifferent([{', '.join(names)}]);")
|
|
862
|
+
lines.append("")
|
|
863
|
+
|
|
864
|
+
ctx = _Ctx(predicates=pred_names, functions=func_names, constants=const_names)
|
|
865
|
+
for i, sentence in enumerate(problem.sentences, start=1):
|
|
866
|
+
lines.append(f"constraint {_formula(sentence, ctx)}; % sentence {i}")
|
|
867
|
+
lines.append("")
|
|
868
|
+
lines.append("solve satisfy;")
|
|
869
|
+
lines.append("")
|
|
870
|
+
|
|
871
|
+
output_parts: List[str] = ['"UFK-SOLUTION-BEGIN\\n"']
|
|
872
|
+
for name in sorted(sig.predicates):
|
|
873
|
+
decl = sig.predicates[name]
|
|
874
|
+
mzn = pred_names[name]
|
|
875
|
+
expr = _flatten_expr(mzn, decl.arity)
|
|
876
|
+
output_parts.append(f'"UFK {mzn} {decl.arity} " ++ {expr} ++ "\\n"')
|
|
877
|
+
for name in sorted(sig.functions):
|
|
878
|
+
decl = sig.functions[name]
|
|
879
|
+
mzn = func_names[name]
|
|
880
|
+
expr = _flatten_expr(mzn, decl.arity)
|
|
881
|
+
output_parts.append(f'"UFK {mzn} {decl.arity} " ++ {expr} ++ "\\n"')
|
|
882
|
+
for name in sorted(const_names):
|
|
883
|
+
mzn = const_names[name]
|
|
884
|
+
output_parts.append(f'"UFK {mzn} 0 " ++ show({mzn}) ++ "\\n"')
|
|
885
|
+
output_parts.append('"UFK-SOLUTION-END\\n"')
|
|
886
|
+
|
|
887
|
+
lines.append("output [")
|
|
888
|
+
lines.append(" " + ",\n ".join(output_parts))
|
|
889
|
+
lines.append("];")
|
|
890
|
+
lines.append("")
|
|
891
|
+
|
|
892
|
+
return "\n".join(lines) + "\n"
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
# =============================================================================
|
|
896
|
+
# Output parsing — the other half of the fixture-testable pair
|
|
897
|
+
# =============================================================================
|
|
898
|
+
|
|
899
|
+
_MznValue = Union[bool, int, list]
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def _parse_mzn_value(text: str) -> _MznValue:
|
|
903
|
+
"""Parse one ``show()``-rendered MiniZinc scalar or flat list.
|
|
904
|
+
|
|
905
|
+
Handles exactly the three shapes :func:`to_minizinc`'s ``output`` item
|
|
906
|
+
can ever produce: ``"true"``/``"false"`` (a bool scalar — a nullary
|
|
907
|
+
predicate, or one entry of a flattened predicate array), a bare integer
|
|
908
|
+
(an int scalar — a constant, a nullary function, or one entry of a
|
|
909
|
+
flattened function array), or a bracketed comma-separated list of either
|
|
910
|
+
(``"[true, false, true]"`` / ``"[0, 1, 1]"``) — the flattened form
|
|
911
|
+
:func:`_flatten_expr` emits for arity >= 1. Never recurses into nested
|
|
912
|
+
brackets: :func:`to_minizinc` never emits one (every array is flattened
|
|
913
|
+
to exactly one dimension before ``show()`` sees it), so a nested bracket
|
|
914
|
+
here would mean the text did not come from this module's own renderer.
|
|
915
|
+
|
|
916
|
+
Raises:
|
|
917
|
+
ValueError: ``text`` is not one of the three recognised shapes.
|
|
918
|
+
"""
|
|
919
|
+
if text == "true":
|
|
920
|
+
return True
|
|
921
|
+
if text == "false":
|
|
922
|
+
return False
|
|
923
|
+
if text.startswith("[") and text.endswith("]"):
|
|
924
|
+
inner = text[1:-1].strip()
|
|
925
|
+
if not inner:
|
|
926
|
+
return []
|
|
927
|
+
return [_parse_mzn_value(part.strip()) for part in inner.split(",")]
|
|
928
|
+
try:
|
|
929
|
+
return int(text)
|
|
930
|
+
except ValueError:
|
|
931
|
+
raise ValueError(f"_parse_mzn_value: unrecognised MiniZinc literal {text!r}.")
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
_UFK_BEGIN = "UFK-SOLUTION-BEGIN"
|
|
935
|
+
_UFK_END = "UFK-SOLUTION-END"
|
|
936
|
+
_UFK_PREFIX = "UFK "
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
def _parse_minizinc_solution(stdout: str) -> Optional[Dict[str, _MznValue]]:
|
|
940
|
+
"""Extract this module's own UFK-tagged solution block from ``stdout``.
|
|
941
|
+
|
|
942
|
+
Returns ``None`` when no ``UFK-SOLUTION-BEGIN`` / ``UFK-SOLUTION-END``
|
|
943
|
+
pair is present at all — the caller's signal that ``minizinc`` did not
|
|
944
|
+
reach the ``output`` item (an ``=====UNSATISFIABLE=====`` /
|
|
945
|
+
``=====UNKNOWN=====`` run, or an infra failure the caller checks for
|
|
946
|
+
separately). Otherwise returns ``{mzn_name: parsed_value}`` for every
|
|
947
|
+
``UFK <mzn_name> <arity> <value>`` line between the sentinels (the
|
|
948
|
+
``<arity>`` field is not otherwise used here — :func:`_atoms_from_solution`
|
|
949
|
+
already knows each symbol's arity from ``signature`` itself — but is kept
|
|
950
|
+
in the wire format as a cheap, human-readable sanity anchor when reading
|
|
951
|
+
a captured transcript by eye).
|
|
952
|
+
|
|
953
|
+
Args:
|
|
954
|
+
stdout: the ``minizinc`` subprocess's captured standard output.
|
|
955
|
+
|
|
956
|
+
Returns:
|
|
957
|
+
The parsed solution dict, or ``None`` if no solution block is present.
|
|
958
|
+
"""
|
|
959
|
+
if _UFK_BEGIN not in stdout or _UFK_END not in stdout:
|
|
960
|
+
return None
|
|
961
|
+
block = stdout[stdout.index(_UFK_BEGIN):stdout.index(_UFK_END)]
|
|
962
|
+
solution: Dict[str, _MznValue] = {}
|
|
963
|
+
for line in block.splitlines():
|
|
964
|
+
line = line.strip()
|
|
965
|
+
if not line.startswith(_UFK_PREFIX):
|
|
966
|
+
continue
|
|
967
|
+
rest = line[len(_UFK_PREFIX):]
|
|
968
|
+
mzn_name, _, tail = rest.partition(" ")
|
|
969
|
+
_arity_str, _, value_str = tail.partition(" ")
|
|
970
|
+
solution[mzn_name] = _parse_mzn_value(value_str.strip())
|
|
971
|
+
return solution
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
def _atoms_from_solution(
|
|
975
|
+
signature: Signature, solution: Dict[str, _MznValue], size: int,
|
|
976
|
+
) -> List[Tuple[str, Tuple[int, ...]]]:
|
|
977
|
+
"""Translate a parsed ``UFK`` solution dict into the shared atom shape.
|
|
978
|
+
|
|
979
|
+
The counterpart of :func:`to_minizinc`'s declaration section: for every
|
|
980
|
+
predicate/function/constant ``signature`` declares, looks up its MiniZinc
|
|
981
|
+
identifier (via the same :func:`_mzn_pred_name` / :func:`_mzn_func_name`
|
|
982
|
+
/ :func:`_mzn_const_name` helpers :func:`to_minizinc` used, so the two
|
|
983
|
+
directions cannot silently drift apart), and expands a flattened array
|
|
984
|
+
value back into ``(index_tuple, value)`` pairs via
|
|
985
|
+
``itertools.product(range(size), repeat=arity)`` — the identical
|
|
986
|
+
generator order :func:`_flatten_expr`'s comprehension emits in the
|
|
987
|
+
``.mzn`` text (last generator fastest), so the ``k``-th list entry lines
|
|
988
|
+
up with the ``k``-th index tuple.
|
|
989
|
+
|
|
990
|
+
Produces exactly the ``(symbol_name, args)`` pairs
|
|
991
|
+
:func:`~unicode_logic_kit.atp.finite_domain.structure_from_solution`
|
|
992
|
+
expects, INCLUDING the ``(k+1)``-tuple total-relation shape for a
|
|
993
|
+
function of arity ``k`` — this function does the pairing;
|
|
994
|
+
``structure_from_solution`` does the functionality/totality validation
|
|
995
|
+
(deliberately not duplicated here — see that function's own docstring).
|
|
996
|
+
|
|
997
|
+
Args:
|
|
998
|
+
signature: the problem's declared vocabulary.
|
|
999
|
+
solution: the dict :func:`_parse_minizinc_solution` produced.
|
|
1000
|
+
size: the domain size the solution was searched at.
|
|
1001
|
+
|
|
1002
|
+
Returns:
|
|
1003
|
+
The atom list, in predicate/function/constant, then sorted-name,
|
|
1004
|
+
order (order is not semantically significant to the caller, but
|
|
1005
|
+
deterministic order keeps this function's own tests reproducible).
|
|
1006
|
+
|
|
1007
|
+
Raises:
|
|
1008
|
+
ValueError: a declared symbol's MiniZinc identifier is missing from
|
|
1009
|
+
``solution``, or its value has the wrong shape (not a bool/list
|
|
1010
|
+
of bools for a predicate, not an int/list of ints for a
|
|
1011
|
+
function/constant, or a list of the wrong length for ``size``
|
|
1012
|
+
and the symbol's arity).
|
|
1013
|
+
"""
|
|
1014
|
+
atoms: List[Tuple[str, Tuple[int, ...]]] = []
|
|
1015
|
+
|
|
1016
|
+
def _expect(mzn_name: str) -> _MznValue:
|
|
1017
|
+
if mzn_name not in solution:
|
|
1018
|
+
raise ValueError(
|
|
1019
|
+
f"_atoms_from_solution: the minizinc solution is missing "
|
|
1020
|
+
f"{mzn_name!r} (declared by the problem's signature)."
|
|
1021
|
+
)
|
|
1022
|
+
return solution[mzn_name]
|
|
1023
|
+
|
|
1024
|
+
for name in sorted(signature.predicates):
|
|
1025
|
+
decl = signature.predicates[name]
|
|
1026
|
+
mzn = _mzn_pred_name(name)
|
|
1027
|
+
value = _expect(mzn)
|
|
1028
|
+
if decl.arity == 0:
|
|
1029
|
+
if not isinstance(value, bool):
|
|
1030
|
+
raise ValueError(
|
|
1031
|
+
f"_atoms_from_solution: predicate {name!r} (arity 0) "
|
|
1032
|
+
f"expects a bool, got {value!r}."
|
|
1033
|
+
)
|
|
1034
|
+
if value:
|
|
1035
|
+
atoms.append((name, ()))
|
|
1036
|
+
continue
|
|
1037
|
+
if not isinstance(value, list):
|
|
1038
|
+
raise ValueError(
|
|
1039
|
+
f"_atoms_from_solution: predicate {name!r} (arity "
|
|
1040
|
+
f"{decl.arity}) expects a flat list, got {value!r}."
|
|
1041
|
+
)
|
|
1042
|
+
index_tuples = list(itertools.product(range(size), repeat=decl.arity))
|
|
1043
|
+
if len(value) != len(index_tuples):
|
|
1044
|
+
raise ValueError(
|
|
1045
|
+
f"_atoms_from_solution: predicate {name!r} expects "
|
|
1046
|
+
f"{len(index_tuples)} entries (size={size}, arity="
|
|
1047
|
+
f"{decl.arity}), got {len(value)}."
|
|
1048
|
+
)
|
|
1049
|
+
for idx_tuple, truth in zip(index_tuples, value):
|
|
1050
|
+
if not isinstance(truth, bool):
|
|
1051
|
+
raise ValueError(
|
|
1052
|
+
f"_atoms_from_solution: predicate {name!r} entry "
|
|
1053
|
+
f"{idx_tuple} expects a bool, got {truth!r}."
|
|
1054
|
+
)
|
|
1055
|
+
if truth:
|
|
1056
|
+
atoms.append((name, idx_tuple))
|
|
1057
|
+
|
|
1058
|
+
for name in sorted(signature.functions):
|
|
1059
|
+
decl = signature.functions[name]
|
|
1060
|
+
mzn = _mzn_func_name(name)
|
|
1061
|
+
value = _expect(mzn)
|
|
1062
|
+
if decl.arity == 0:
|
|
1063
|
+
if not isinstance(value, int) or isinstance(value, bool):
|
|
1064
|
+
raise ValueError(
|
|
1065
|
+
f"_atoms_from_solution: function {name!r} (arity 0) "
|
|
1066
|
+
f"expects an int, got {value!r}."
|
|
1067
|
+
)
|
|
1068
|
+
atoms.append((name, (value,)))
|
|
1069
|
+
continue
|
|
1070
|
+
if not isinstance(value, list):
|
|
1071
|
+
raise ValueError(
|
|
1072
|
+
f"_atoms_from_solution: function {name!r} (arity "
|
|
1073
|
+
f"{decl.arity}) expects a flat list, got {value!r}."
|
|
1074
|
+
)
|
|
1075
|
+
index_tuples = list(itertools.product(range(size), repeat=decl.arity))
|
|
1076
|
+
if len(value) != len(index_tuples):
|
|
1077
|
+
raise ValueError(
|
|
1078
|
+
f"_atoms_from_solution: function {name!r} expects "
|
|
1079
|
+
f"{len(index_tuples)} entries (size={size}, arity="
|
|
1080
|
+
f"{decl.arity}), got {len(value)}."
|
|
1081
|
+
)
|
|
1082
|
+
for idx_tuple, result in zip(index_tuples, value):
|
|
1083
|
+
if not isinstance(result, int) or isinstance(result, bool):
|
|
1084
|
+
raise ValueError(
|
|
1085
|
+
f"_atoms_from_solution: function {name!r} entry "
|
|
1086
|
+
f"{idx_tuple} expects an int result, got {result!r}."
|
|
1087
|
+
)
|
|
1088
|
+
atoms.append((name, idx_tuple + (result,)))
|
|
1089
|
+
|
|
1090
|
+
for name in sorted(signature.constants):
|
|
1091
|
+
mzn = _mzn_const_name(name)
|
|
1092
|
+
value = _expect(mzn)
|
|
1093
|
+
if not isinstance(value, int) or isinstance(value, bool):
|
|
1094
|
+
raise ValueError(
|
|
1095
|
+
f"_atoms_from_solution: constant {name!r} expects an int, "
|
|
1096
|
+
f"got {value!r}."
|
|
1097
|
+
)
|
|
1098
|
+
atoms.append((name, (value,)))
|
|
1099
|
+
|
|
1100
|
+
return atoms
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
# =============================================================================
|
|
1104
|
+
# Subprocess plumbing
|
|
1105
|
+
# =============================================================================
|
|
1106
|
+
|
|
1107
|
+
def _environment_for(binary: str) -> Optional[Dict[str, str]]:
|
|
1108
|
+
"""The environment to run ``binary`` in: the caller's, with the binary's own
|
|
1109
|
+
folder (and its ``bin`` subfolder) put first on ``PATH``.
|
|
1110
|
+
|
|
1111
|
+
MiniZinc starts its solver as a second program (``bin/fzn-gecode``), and on
|
|
1112
|
+
Windows that program loads libraries that lie next to ``minizinc.exe``. An
|
|
1113
|
+
installer puts that folder on ``PATH``; a binary reached only through
|
|
1114
|
+
``$UFK_MINIZINC`` has no such entry, and the bundled default solver then
|
|
1115
|
+
ends at once with ``=====ERROR=====`` and an empty error stream (measured
|
|
1116
|
+
with MiniZinc 2.8.4: Gecode fails that way, Chuffed does not). ``None``
|
|
1117
|
+
(inherit the environment unchanged) when ``binary`` is a bare command
|
|
1118
|
+
name, which the shell resolved through ``PATH`` already.
|
|
1119
|
+
"""
|
|
1120
|
+
folder = os.path.dirname(binary)
|
|
1121
|
+
if not folder:
|
|
1122
|
+
return None
|
|
1123
|
+
env = dict(os.environ)
|
|
1124
|
+
own = [folder, os.path.join(folder, "bin")]
|
|
1125
|
+
env["PATH"] = os.pathsep.join(own + [env.get("PATH", "")])
|
|
1126
|
+
return env
|
|
1127
|
+
|
|
1128
|
+
|
|
1129
|
+
def _run_minizinc(
|
|
1130
|
+
model_path: str, binary: str, solver: str, time_limit_ms: int,
|
|
1131
|
+
) -> Tuple[str, str, bool]:
|
|
1132
|
+
"""Run ``minizinc`` on ``model_path``; return ``(stdout, stderr, timed_out)``.
|
|
1133
|
+
|
|
1134
|
+
``--time-limit`` (milliseconds) is MiniZinc's OWN search budget, so a
|
|
1135
|
+
solver that hits it exits cleanly with ``=====UNKNOWN=====`` in its
|
|
1136
|
+
stdout rather than being killed — the Python-side ``subprocess.run``
|
|
1137
|
+
timeout is set ten seconds beyond that as a hard backstop against a
|
|
1138
|
+
solver that ignores its own budget (mirrors
|
|
1139
|
+
:mod:`atp.eprover_backend`'s ``_run_tptp_prover`` doing the identical
|
|
1140
|
+
thing for E's ``--cpu-limit``), not the primary timeout mechanism.
|
|
1141
|
+
"""
|
|
1142
|
+
args = [binary, "--solver", solver, "--time-limit", str(time_limit_ms), model_path]
|
|
1143
|
+
try:
|
|
1144
|
+
result = subprocess.run(
|
|
1145
|
+
args, capture_output=True, text=True,
|
|
1146
|
+
timeout=(time_limit_ms / 1000.0) + 10,
|
|
1147
|
+
env=_environment_for(binary),
|
|
1148
|
+
)
|
|
1149
|
+
return result.stdout or "", result.stderr or "", False
|
|
1150
|
+
except subprocess.TimeoutExpired:
|
|
1151
|
+
return "", "", True
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
# =============================================================================
|
|
1155
|
+
# MinizincBackend
|
|
1156
|
+
# =============================================================================
|
|
1157
|
+
|
|
1158
|
+
class MinizincBackend(ProverBackend):
|
|
1159
|
+
"""Bounded CP finite-domain refutation search via the ``minizinc`` CLI.
|
|
1160
|
+
|
|
1161
|
+
Registry name ``"minizinc"``. Refutation-only, per the module docstring
|
|
1162
|
+
and :mod:`~unicode_logic_kit.atp.finite_domain`'s own "ONE RULE" — searches
|
|
1163
|
+
domain sizes ``1 … max_size`` in turn for a model of ``premises ∧ ¬φ``;
|
|
1164
|
+
the first one found is independently re-verified
|
|
1165
|
+
(:func:`~unicode_logic_kit.atp.finite_domain.verify_model`) and returned as
|
|
1166
|
+
REFUTED (or, if verification fails, ERROR/``"infra"`` — never a
|
|
1167
|
+
countermodel this module could not confirm itself). Exhausting every
|
|
1168
|
+
size without a model is UNKNOWN/``"bound_hit"``; running out of the
|
|
1169
|
+
``timeout`` budget partway through the size sweep is UNKNOWN/``"timeout"``
|
|
1170
|
+
— the two are DELIBERATELY different ``reason`` values (see
|
|
1171
|
+
:mod:`~unicode_logic_kit.atp.protocol`'s module docstring on why
|
|
1172
|
+
``bound_hit`` and ``timeout`` must never be conflated): only the former
|
|
1173
|
+
means "the whole search space up to max_size was actually covered".
|
|
1174
|
+
"""
|
|
1175
|
+
|
|
1176
|
+
name = "minizinc"
|
|
1177
|
+
logics = frozenset({"fol"})
|
|
1178
|
+
external = True
|
|
1179
|
+
|
|
1180
|
+
def available(self) -> bool:
|
|
1181
|
+
"""Pure discovery: see :func:`minizinc_available`."""
|
|
1182
|
+
return minizinc_available()
|
|
1183
|
+
|
|
1184
|
+
def available_for(self, options: dict) -> bool:
|
|
1185
|
+
"""Whether the binary the run will use is there: ``minizinc_path=`` when the
|
|
1186
|
+
call names one (it must resolve to something the run can start, as an explicit
|
|
1187
|
+
path or as a name on ``PATH``; on Windows the installed ``minizinc.exe`` may be
|
|
1188
|
+
named without its extension, as ``subprocess`` starts it), else the discovery
|
|
1189
|
+
of :func:`minizinc_available` (``$UFK_MINIZINC``, then ``PATH``).
|
|
1190
|
+
:meth:`decide` reads ``minizinc_path=`` before it looks anywhere else, so a
|
|
1191
|
+
call that names a working binary is answered even where nothing is
|
|
1192
|
+
discoverable, and a call that names a missing one is refused here, by name,
|
|
1193
|
+
instead of failing inside the run."""
|
|
1194
|
+
path = options.get("minizinc_path")
|
|
1195
|
+
if path:
|
|
1196
|
+
return _native_command_exists(path)
|
|
1197
|
+
return self.available()
|
|
1198
|
+
|
|
1199
|
+
def decide(self, formula: Node, premises: Sequence[Node] = (),
|
|
1200
|
+
timeout: int = 10000, **options) -> Verdict:
|
|
1201
|
+
"""Decide ``premises ⊨ formula`` by CP finite-domain refutation search.
|
|
1202
|
+
|
|
1203
|
+
Args:
|
|
1204
|
+
formula: the goal.
|
|
1205
|
+
premises: entailment premises (validity search when empty). A
|
|
1206
|
+
free variable of the premises or the goal is a parameter:
|
|
1207
|
+
one unknown element, the same in all of them, as in
|
|
1208
|
+
``ClingoBackend.decide``. With ``all_different=True`` such
|
|
1209
|
+
a problem is answered ``unknown`` / ``unsupported``.
|
|
1210
|
+
timeout: milliseconds, the TOTAL wall-clock budget across every
|
|
1211
|
+
domain size attempted (not per-size — a size that starts
|
|
1212
|
+
with little budget left gets little budget, and the search
|
|
1213
|
+
stops rather than overrunning; see the class docstring's
|
|
1214
|
+
``bound_hit`` vs ``timeout`` distinction).
|
|
1215
|
+
**options: ``max_size`` (default ``4``, matching
|
|
1216
|
+
:class:`~unicode_logic_kit.atp.protocol.ModelFinderBackend`'s
|
|
1217
|
+
own default so a differential test between the two can use
|
|
1218
|
+
matching bounds out of the box) — the largest domain size
|
|
1219
|
+
tried; ``solver`` (default ``"gecode"``, MiniZinc's bundled
|
|
1220
|
+
default) — the ``--solver`` id passed to the CLI;
|
|
1221
|
+
``all_different`` (default ``False``) — forwarded to
|
|
1222
|
+
:class:`~unicode_logic_kit.atp.finite_domain.FiniteDomainProblem`;
|
|
1223
|
+
``minizinc_path`` overrides binary discovery (see
|
|
1224
|
+
:func:`_minizinc_binary`).
|
|
1225
|
+
|
|
1226
|
+
Returns:
|
|
1227
|
+
A :class:`~unicode_logic_kit.atp.protocol.Verdict`. ``status`` is
|
|
1228
|
+
NEVER ``"proved"`` (see the class docstring). REFUTED carries
|
|
1229
|
+
``countermodel = {"kind": "finite_structure", "data":
|
|
1230
|
+
structure.to_dict()}`` (round-trippable via
|
|
1231
|
+
:func:`~unicode_logic_kit.semantics.structures.structure_from_dict`
|
|
1232
|
+
— fixing the bare-``repr`` weakness
|
|
1233
|
+
:class:`~unicode_logic_kit.atp.protocol.ModelFinderBackend` has).
|
|
1234
|
+
|
|
1235
|
+
Raises:
|
|
1236
|
+
BackendUnavailable: no ``minizinc`` binary is reachable (see
|
|
1237
|
+
:func:`_minizinc_binary`) — mirrors
|
|
1238
|
+
:class:`~unicode_logic_kit.atp.protocol.Prover9Backend` /
|
|
1239
|
+
:class:`~unicode_logic_kit.atp.protocol.VampireBackend`
|
|
1240
|
+
raising this directly from ``decide()`` as a defensive
|
|
1241
|
+
re-check, not only from :func:`~unicode_logic_kit.atp.protocol.run_backend`.
|
|
1242
|
+
"""
|
|
1243
|
+
binary = options.pop("minizinc_path", None) or _minizinc_binary()
|
|
1244
|
+
if binary is None:
|
|
1245
|
+
raise BackendUnavailable(
|
|
1246
|
+
"minizinc: no binary found (set $UFK_MINIZINC or put "
|
|
1247
|
+
"'minizinc' on PATH) — see "
|
|
1248
|
+
"unicode_logic_kit/atp/minizinc_backend.py for the acquisition "
|
|
1249
|
+
"path."
|
|
1250
|
+
)
|
|
1251
|
+
max_size = options.pop("max_size", 4)
|
|
1252
|
+
solver = options.pop("solver", "gecode")
|
|
1253
|
+
all_different = options.pop("all_different", False)
|
|
1254
|
+
|
|
1255
|
+
# A free variable is a parameter of the problem: one unknown element, the same in
|
|
1256
|
+
# the premises and the goal. Each is replaced here, in all of them together, by a
|
|
1257
|
+
# constant of its own name, and only then is the goal negated (¬∀x φ(x) is not
|
|
1258
|
+
# ∀x ¬φ(x)); a countermodel reports the element under the variable's name. No
|
|
1259
|
+
# premise is closed universally: P(x) does not entail P(alpha).
|
|
1260
|
+
try:
|
|
1261
|
+
read, parameters = parameterize(list(premises) + [formula], after_variables=True)
|
|
1262
|
+
except NotImplementedError as exc:
|
|
1263
|
+
return Verdict(UNKNOWN, self.name, reason="unsupported", detail=str(exc))
|
|
1264
|
+
if all_different and parameters:
|
|
1265
|
+
# The constants are pairwise distinct under this option, a parameter may equal
|
|
1266
|
+
# any of them, and the model states distinctness over every constant it declares.
|
|
1267
|
+
return Verdict(
|
|
1268
|
+
UNKNOWN, self.name, reason="unsupported",
|
|
1269
|
+
detail=("minizinc: all_different=True with the free variable"
|
|
1270
|
+
f"{'s' if len(parameters) > 1 else ''} "
|
|
1271
|
+
f"{', '.join(repr(name) for name in sorted(parameters))}: a parameter "
|
|
1272
|
+
"is not one of the pairwise distinct constants, and this route cannot "
|
|
1273
|
+
"leave it out of the distinctness constraint. Bind the variable or "
|
|
1274
|
+
"drop all_different."))
|
|
1275
|
+
sentences: Tuple[Node, ...] = tuple(read[:-1]) + (Not(read[-1]),)
|
|
1276
|
+
# Many-sorted input is relativised to plain classical FOL HERE, once,
|
|
1277
|
+
# before Signature.from_formulas / to_minizinc's own fragment_check
|
|
1278
|
+
# call ever see it -- see finite_domain.lower_msfol's own docstring
|
|
1279
|
+
# and this module's "Many-sorted input" section. A no-op for every
|
|
1280
|
+
# unsorted-only caller (the pre-existing test suite): returns
|
|
1281
|
+
# `sentences` untouched when nothing sorted is present.
|
|
1282
|
+
sentences = lower_msfol(sentences)
|
|
1283
|
+
|
|
1284
|
+
try:
|
|
1285
|
+
signature = Signature.from_formulas(sentences)
|
|
1286
|
+
except (TypeError, ValueError) as exc:
|
|
1287
|
+
return Verdict(UNKNOWN, self.name, reason="unsupported",
|
|
1288
|
+
detail=f"could not derive a signature: {exc}")
|
|
1289
|
+
|
|
1290
|
+
start = time.perf_counter()
|
|
1291
|
+
budget_s = max(0.001, timeout / 1000.0)
|
|
1292
|
+
|
|
1293
|
+
for size in range(1, max_size + 1):
|
|
1294
|
+
elapsed = time.perf_counter() - start
|
|
1295
|
+
remaining_s = budget_s - elapsed
|
|
1296
|
+
if remaining_s <= 0:
|
|
1297
|
+
return Verdict(
|
|
1298
|
+
UNKNOWN, self.name, reason="timeout", wall_time=elapsed,
|
|
1299
|
+
detail=f"time budget exhausted before size {size} "
|
|
1300
|
+
f"(max_size={max_size})",
|
|
1301
|
+
)
|
|
1302
|
+
|
|
1303
|
+
try:
|
|
1304
|
+
problem = FiniteDomainProblem(
|
|
1305
|
+
sentences, size, signature=signature,
|
|
1306
|
+
all_different=all_different,
|
|
1307
|
+
)
|
|
1308
|
+
except (TypeError, ValueError) as exc:
|
|
1309
|
+
return Verdict(UNKNOWN, self.name, reason="unsupported",
|
|
1310
|
+
wall_time=time.perf_counter() - start,
|
|
1311
|
+
detail=str(exc))
|
|
1312
|
+
|
|
1313
|
+
try:
|
|
1314
|
+
model_text = to_minizinc(problem)
|
|
1315
|
+
except NotImplementedError as exc:
|
|
1316
|
+
# The same NotImplementedError would recur at every size (it
|
|
1317
|
+
# depends only on `sentences`, never on `size`), so there is
|
|
1318
|
+
# no point looping further — return immediately.
|
|
1319
|
+
return Verdict(UNKNOWN, self.name, reason="unsupported",
|
|
1320
|
+
wall_time=time.perf_counter() - start, detail=str(exc))
|
|
1321
|
+
|
|
1322
|
+
time_limit_ms = max(1, int(remaining_s * 1000))
|
|
1323
|
+
tmp_path = None
|
|
1324
|
+
try:
|
|
1325
|
+
with tempfile.NamedTemporaryFile(
|
|
1326
|
+
mode="w", suffix=".mzn", delete=False, encoding="utf-8",
|
|
1327
|
+
) as tmp:
|
|
1328
|
+
tmp.write(model_text)
|
|
1329
|
+
tmp_path = tmp.name
|
|
1330
|
+
stdout, stderr, timed_out = _run_minizinc(
|
|
1331
|
+
tmp_path, binary, solver, time_limit_ms)
|
|
1332
|
+
except OSError as exc:
|
|
1333
|
+
return Verdict(ERROR, self.name, reason="infra",
|
|
1334
|
+
wall_time=time.perf_counter() - start,
|
|
1335
|
+
detail=f"{type(exc).__name__}: {exc}")
|
|
1336
|
+
finally:
|
|
1337
|
+
if tmp_path is not None:
|
|
1338
|
+
try:
|
|
1339
|
+
os.unlink(tmp_path)
|
|
1340
|
+
except OSError:
|
|
1341
|
+
pass
|
|
1342
|
+
|
|
1343
|
+
if timed_out:
|
|
1344
|
+
return Verdict(
|
|
1345
|
+
UNKNOWN, self.name, reason="timeout",
|
|
1346
|
+
wall_time=time.perf_counter() - start,
|
|
1347
|
+
detail=f"minizinc did not finish size {size} within the "
|
|
1348
|
+
"time budget",
|
|
1349
|
+
)
|
|
1350
|
+
if "=====UNSATISFIABLE=====" in stdout:
|
|
1351
|
+
continue
|
|
1352
|
+
if "=====UNKNOWN=====" in stdout:
|
|
1353
|
+
return Verdict(
|
|
1354
|
+
UNKNOWN, self.name, reason="timeout",
|
|
1355
|
+
wall_time=time.perf_counter() - start,
|
|
1356
|
+
detail=f"minizinc's own --time-limit expired searching "
|
|
1357
|
+
f"size {size} without determining satisfiability",
|
|
1358
|
+
)
|
|
1359
|
+
|
|
1360
|
+
solution = _parse_minizinc_solution(stdout)
|
|
1361
|
+
if solution is None:
|
|
1362
|
+
return Verdict(
|
|
1363
|
+
ERROR, self.name, reason="infra",
|
|
1364
|
+
wall_time=time.perf_counter() - start,
|
|
1365
|
+
detail="minizinc produced no recognised status or "
|
|
1366
|
+
f"solution at size {size}: stdout="
|
|
1367
|
+
f"{stdout[-500:]!r} stderr={stderr[-500:]!r}",
|
|
1368
|
+
)
|
|
1369
|
+
try:
|
|
1370
|
+
atoms = _atoms_from_solution(problem.signature, solution, size)
|
|
1371
|
+
structure = structure_from_solution(
|
|
1372
|
+
problem.signature, atoms, size,
|
|
1373
|
+
all_different=problem.all_different,
|
|
1374
|
+
)
|
|
1375
|
+
except (TypeError, ValueError) as exc:
|
|
1376
|
+
return Verdict(
|
|
1377
|
+
ERROR, self.name, reason="infra",
|
|
1378
|
+
wall_time=time.perf_counter() - start,
|
|
1379
|
+
detail="could not reconstruct a structure from "
|
|
1380
|
+
f"minizinc's solution: {exc}",
|
|
1381
|
+
)
|
|
1382
|
+
verify_reason = verify_model(structure, sentences)
|
|
1383
|
+
if verify_reason is not None:
|
|
1384
|
+
return Verdict(
|
|
1385
|
+
ERROR, self.name, reason="infra",
|
|
1386
|
+
wall_time=time.perf_counter() - start,
|
|
1387
|
+
detail="minizinc reported a solution that failed "
|
|
1388
|
+
"independent verification -- refusing to report "
|
|
1389
|
+
f"REFUTED: {verify_reason}",
|
|
1390
|
+
)
|
|
1391
|
+
return Verdict(
|
|
1392
|
+
REFUTED, self.name, wall_time=time.perf_counter() - start,
|
|
1393
|
+
countermodel={"kind": "finite_structure", "data": structure.to_dict()},
|
|
1394
|
+
detail=f"finite countermodel of size {size} found by "
|
|
1395
|
+
f"minizinc (solver={solver})",
|
|
1396
|
+
)
|
|
1397
|
+
|
|
1398
|
+
return Verdict(
|
|
1399
|
+
UNKNOWN, self.name, reason="bound_hit",
|
|
1400
|
+
wall_time=time.perf_counter() - start,
|
|
1401
|
+
detail=f"no countermodel up to size {max_size}",
|
|
1402
|
+
)
|