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,489 @@
|
|
|
1
|
+
"""Portfolio scheduling: race several backends, take the first agreed answer.
|
|
2
|
+
|
|
3
|
+
:mod:`unicode_logic_kit.api`'s ``prove()`` runs a fixed backend chain IN ORDER,
|
|
4
|
+
one call at a time — cheap, deterministic, and the right default. This module
|
|
5
|
+
is the opposite tool: given an EXPLICIT list of backends, it runs them
|
|
6
|
+
CONCURRENTLY and returns as soon as enough of them agree. It is deliberately
|
|
7
|
+
opt-in (:func:`portfolio_prove` has no default backend list, unlike
|
|
8
|
+
``default_chain``) — racing backends burns more CPU than a sequential chain
|
|
9
|
+
for the same query, so a caller must name the routes it wants raced.
|
|
10
|
+
|
|
11
|
+
Why processes, not threads
|
|
12
|
+
---------------------------
|
|
13
|
+
Every backend that goes through Z3 shares one process-global Z3 context;
|
|
14
|
+
:class:`z3.Solver` is documented as not thread-safe, so two ``decide()``
|
|
15
|
+
calls racing in threads of the SAME process can corrupt each other's state
|
|
16
|
+
invisibly. Equally important: a native solver call in flight (Z3's C core,
|
|
17
|
+
a Prover9/Vampire subprocess wait) cannot be cancelled from a Python thread
|
|
18
|
+
— there is no safe way to interrupt it once started. A separate OS process
|
|
19
|
+
sidesteps both problems: no shared solver state, and "the loser is still
|
|
20
|
+
running" is handled by simply not waiting for it, never by trying to kill
|
|
21
|
+
its native call out from under it. Hence ``concurrent.futures.
|
|
22
|
+
ProcessPoolExecutor``, exactly like :func:`unicode_logic_kit.eval.batch.
|
|
23
|
+
batch_decide` — and the same project-wide cap applies (see memory: "cap all
|
|
24
|
+
parallelism at 8"): ``jobs`` is clamped to ``min(jobs, 8)``.
|
|
25
|
+
|
|
26
|
+
Contract
|
|
27
|
+
--------
|
|
28
|
+
* ``backends`` is REQUIRED — an unknown name raises ``ValueError`` and a
|
|
29
|
+
known-but-unavailable one raises
|
|
30
|
+
:class:`~unicode_logic_kit.atp.protocol.BackendUnavailable`, checked for
|
|
31
|
+
EVERY name before anything is spawned (never a partial run that discovers
|
|
32
|
+
a bad name three processes in). Availability is asked for the route the
|
|
33
|
+
call's own options select (``use_wsl=``, ``minizinc_path=``, ...), as
|
|
34
|
+
``run_backend`` asks it.
|
|
35
|
+
* The options are planned exactly as ``prove()`` plans them
|
|
36
|
+
(:func:`~unicode_logic_kit.atp.protocol.plan_options`): a member is handed only
|
|
37
|
+
the options it reads, an option no member reads is a ``ValueError``, and a
|
|
38
|
+
member that cannot read an option that changes the question (a ``subsorts=``
|
|
39
|
+
edge, a modal ``frame=``) is not run -- it is listed as
|
|
40
|
+
``unknown/unsupported`` and casts no vote, so no member ever answers a
|
|
41
|
+
question other than the one that was asked.
|
|
42
|
+
* ``require_agreement=1`` (the default): the first DEFINITIVE (proved /
|
|
43
|
+
refuted) verdict wins immediately; every other backend is left running
|
|
44
|
+
(see above) and the executor is shut down without waiting for them.
|
|
45
|
+
* ``require_agreement=n>1``: verdicts are tallied by status until ``n``
|
|
46
|
+
backends report the SAME definitive status; the winning ``Verdict`` carries
|
|
47
|
+
``agreement`` as the tuple of backend names that concurred.
|
|
48
|
+
* **Contradiction is a soundness alarm, not something to smooth over.** If
|
|
49
|
+
one backend reports PROVED and another reports REFUTED for the same
|
|
50
|
+
query, that is a live bug in one of the two calculi (or their translation
|
|
51
|
+
to/from the shared AST) — auto-resolving it (e.g. "trust the majority")
|
|
52
|
+
would hide exactly the kind of bug this module exists to catch. The
|
|
53
|
+
result is ``Verdict(status="error", reason="infra", ...)`` naming both
|
|
54
|
+
backends, never a silent pick.
|
|
55
|
+
* No backend reaches a definitive verdict within ``require_agreement``
|
|
56
|
+
copies → an UNKNOWN verdict from the pseudo-backend ``"portfolio"``,
|
|
57
|
+
``detail`` summarising every backend's own answer (mirrors ``prove()``'s
|
|
58
|
+
``"chain"`` pseudo-backend, including its treatment of a member that
|
|
59
|
+
FAILED: quoted by its own message, and an ERROR verdict when every member
|
|
60
|
+
failed).
|
|
61
|
+
* ``jobs=1`` (forced, or the natural result of a single-element
|
|
62
|
+
``backends``) never touches ``ProcessPoolExecutor`` — it runs the SAME
|
|
63
|
+
backends sequentially in THIS process. This is not just an optimisation:
|
|
64
|
+
it is also the only way to portfolio-race a backend that was registered
|
|
65
|
+
ad hoc in the calling process (e.g. a test double) and would not be
|
|
66
|
+
importable by name inside a spawned worker.
|
|
67
|
+
* **A caller's error is raised, whatever ``jobs`` is.** A member that refuses the
|
|
68
|
+
CALL — a modal ``frame=`` it does not know, a ``premise_names=`` list that does not
|
|
69
|
+
match the premises — raises ``ValueError`` (or
|
|
70
|
+
:class:`~unicode_logic_kit.atp.protocol.BackendUnavailable`) out of
|
|
71
|
+
:func:`portfolio_prove`: :func:`~unicode_logic_kit.atp.protocol.run_backend` keeps a
|
|
72
|
+
caller's error loud, and so do the sequential path and ``api.prove``. In a race the
|
|
73
|
+
error that is raised is the first one a member reports, and a member that has
|
|
74
|
+
already won when another reports its error wins, as the first member of
|
|
75
|
+
``api.prove``'s chain that answers does. Only a failure that is NOT the caller's — a
|
|
76
|
+
worker process that died, an answer that cannot be read back — is recorded as
|
|
77
|
+
``error`` / ``infra`` and quoted in the collective verdict.
|
|
78
|
+
* The premises are counted as ``api.prove`` counts them: the caller's own premises are
|
|
79
|
+
the ones ``premise_names=`` names and the ones an index (``relevant_premises``, a
|
|
80
|
+
Z3 core) refers to; the sentences of ``signature=`` and the side axioms of a
|
|
81
|
+
:class:`~unicode_logic_kit.logic.Sentence` are background, named for the writer and
|
|
82
|
+
never reported back.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
86
|
+
from dataclasses import replace
|
|
87
|
+
from typing import Dict, List, Mapping, Optional, Sequence, Tuple
|
|
88
|
+
|
|
89
|
+
from ..fol.nodes import Node
|
|
90
|
+
from ..fol.serialize import deserialize as _fol_deserialize
|
|
91
|
+
from ..fol.serialize import serialize as _fol_serialize
|
|
92
|
+
from .protocol import (
|
|
93
|
+
ERROR, PROVED, REFUTED, UNKNOWN,
|
|
94
|
+
BackendUnavailable, Verdict, get_backend, plan_options, run_backend,
|
|
95
|
+
_available_for, _no_definitive_verdict,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
__all__ = ["portfolio_prove"]
|
|
99
|
+
|
|
100
|
+
# Project-wide parallelism cap (see memory: "cap all parallelism at 8").
|
|
101
|
+
_MAX_JOBS = 8
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
# Agreement tally: shared by the sequential and process-pool paths
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
class _Contradiction(Exception):
|
|
109
|
+
"""Raised internally when two definitive verdicts disagree.
|
|
110
|
+
|
|
111
|
+
Carries the PROVED and REFUTED verdicts that clashed so the caller can
|
|
112
|
+
build the soundness-alarm ``Verdict`` naming both backends.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
def __init__(self, proved: Verdict, refuted: Verdict):
|
|
116
|
+
super().__init__(f"{proved.backend} says proved, {refuted.backend} says refuted")
|
|
117
|
+
self.proved = proved
|
|
118
|
+
self.refuted = refuted
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _feed(agreeing: Dict[str, List[Verdict]], verdict: Verdict,
|
|
122
|
+
require_agreement: int) -> Optional[Verdict]:
|
|
123
|
+
"""Fold one DEFINITIVE verdict into the running tally.
|
|
124
|
+
|
|
125
|
+
Returns the winning ``Verdict`` (agreement reached), raises
|
|
126
|
+
:class:`_Contradiction` (a proved/refuted split), or returns ``None``
|
|
127
|
+
(keep collecting) — the three, and only three, outcomes of adding one
|
|
128
|
+
more definitive vote.
|
|
129
|
+
"""
|
|
130
|
+
if verdict.status == PROVED and agreeing.get(REFUTED):
|
|
131
|
+
raise _Contradiction(verdict, agreeing[REFUTED][0])
|
|
132
|
+
if verdict.status == REFUTED and agreeing.get(PROVED):
|
|
133
|
+
raise _Contradiction(agreeing[PROVED][0], verdict)
|
|
134
|
+
group = agreeing.setdefault(verdict.status, [])
|
|
135
|
+
group.append(verdict)
|
|
136
|
+
if len(group) >= require_agreement:
|
|
137
|
+
first = group[0]
|
|
138
|
+
if len(group) == 1:
|
|
139
|
+
return first
|
|
140
|
+
return replace(first, agreement=tuple(v.backend for v in group))
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _contradiction_verdict(logic: str, proved: Verdict, refuted: Verdict) -> Verdict:
|
|
145
|
+
"""Build the ERROR verdict for a proved/refuted split between backends."""
|
|
146
|
+
return Verdict(
|
|
147
|
+
ERROR, "portfolio", logic=logic, reason="infra",
|
|
148
|
+
detail=(f"soundness alarm: {proved.backend!r} reports proved but "
|
|
149
|
+
f"{refuted.backend!r} reports refuted for the same query — "
|
|
150
|
+
f"not auto-resolved, investigate both backends"),
|
|
151
|
+
agreement=(proved.backend, refuted.backend),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _unknown_verdict(logic: str, verdicts: List[Verdict]) -> Verdict:
|
|
156
|
+
"""Build the collective verdict when nobody reached agreement.
|
|
157
|
+
|
|
158
|
+
The same rule as ``api.prove``'s chain (they must not answer one question
|
|
159
|
+
two ways): UNKNOWN, with a member that failed quoted by its own message,
|
|
160
|
+
and ERROR when every member failed -- see
|
|
161
|
+
:func:`~unicode_logic_kit.atp.protocol._no_definitive_verdict`.
|
|
162
|
+
"""
|
|
163
|
+
return _no_definitive_verdict("portfolio", logic, verdicts, "empty backend list")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# ---------------------------------------------------------------------------
|
|
167
|
+
# Sequential path (jobs == 1): same process, no pool
|
|
168
|
+
# ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
def _refused_verdict(name: str, logic: str, refusal: str) -> Verdict:
|
|
171
|
+
"""The verdict of a member that was not run because it cannot read an option
|
|
172
|
+
that changes the question (the same verdict ``api.prove`` lists for it)."""
|
|
173
|
+
return Verdict(UNKNOWN, name, logic=logic, reason="unsupported", detail=refusal)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _run_sequential(formula: Node, premises: Sequence[Node], backends: Sequence[str],
|
|
177
|
+
logic: str, timeout: int, require_agreement: int,
|
|
178
|
+
plan: Dict[str, Tuple[dict, Optional[str]]]) -> Verdict:
|
|
179
|
+
"""Run ``backends`` one at a time in THIS process.
|
|
180
|
+
|
|
181
|
+
Used whenever ``jobs`` resolves to 1: either the caller forced it (e.g.
|
|
182
|
+
to portfolio-race a locally-registered test backend that a spawned
|
|
183
|
+
worker could not import) or a single-element ``backends`` made pooling
|
|
184
|
+
pointless. ``plan`` is what :func:`~unicode_logic_kit.atp.protocol.plan_options`
|
|
185
|
+
decided: the options each member is handed, or the reason it is not run.
|
|
186
|
+
"""
|
|
187
|
+
agreeing: Dict[str, List[Verdict]] = {}
|
|
188
|
+
verdicts: List[Verdict] = []
|
|
189
|
+
for name in backends:
|
|
190
|
+
passed, refusal = plan[name]
|
|
191
|
+
if refusal is not None:
|
|
192
|
+
verdicts.append(_refused_verdict(name, logic, refusal))
|
|
193
|
+
continue
|
|
194
|
+
extra = dict(passed)
|
|
195
|
+
if name == "isabelle":
|
|
196
|
+
extra["logic"] = logic
|
|
197
|
+
verdict = run_backend(name, formula, premises, timeout=timeout, **extra)
|
|
198
|
+
verdicts.append(verdict)
|
|
199
|
+
if verdict.is_definitive:
|
|
200
|
+
try:
|
|
201
|
+
won = _feed(agreeing, verdict, require_agreement)
|
|
202
|
+
except _Contradiction as exc:
|
|
203
|
+
return _contradiction_verdict(logic, exc.proved, exc.refuted)
|
|
204
|
+
if won is not None:
|
|
205
|
+
return won
|
|
206
|
+
return _unknown_verdict(logic, verdicts)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# ---------------------------------------------------------------------------
|
|
210
|
+
# Process-pool path (jobs > 1)
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
def _verdict_from_dict(d: dict) -> Verdict:
|
|
214
|
+
"""Reconstruct a ``Verdict`` from ``Verdict.to_dict()`` — round-trips
|
|
215
|
+
EVERY field, including ``relevant_premises`` and ``solver_version``
|
|
216
|
+
(both were silently dropped here before K1: a real bug, since this is
|
|
217
|
+
the process-pool path's only way back from a worker's JSON-safe dict to
|
|
218
|
+
a live ``Verdict``, so both fields vanished on every ``jobs>1``
|
|
219
|
+
portfolio race — see ``tests/test_portfolio.py``'s full-fields round
|
|
220
|
+
trip, which pins ``Verdict.to_dict()`` unchanged by this reconstruction).
|
|
221
|
+
"""
|
|
222
|
+
return Verdict(
|
|
223
|
+
status=d["status"], backend=d["backend"], logic=d["logic"],
|
|
224
|
+
reason=d["reason"], szs_status=d["szs_status"], wall_time=d["wall_time"],
|
|
225
|
+
solver_version=d["solver_version"],
|
|
226
|
+
countermodel=d["countermodel"], proof=d["proof"], detail=d["detail"],
|
|
227
|
+
agreement=tuple(d["agreement"]),
|
|
228
|
+
relevant_premises=(tuple(d["relevant_premises"])
|
|
229
|
+
if d["relevant_premises"] is not None else None),
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _crash_verdict(name: str, logic: str, exc: BaseException) -> Verdict:
|
|
234
|
+
"""The verdict recorded for a member whose worker failed for a reason that is not the
|
|
235
|
+
caller's: a broken pool, an answer that does not come back as a verdict."""
|
|
236
|
+
return Verdict(ERROR, name, logic=logic, reason="infra",
|
|
237
|
+
detail=f"{type(exc).__name__}: {exc}")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _worker_decide(payload: dict) -> dict:
|
|
241
|
+
"""``ProcessPoolExecutor`` target — MUST stay module-level for Windows
|
|
242
|
+
spawn (it pickles the target by qualified name, not by closure).
|
|
243
|
+
|
|
244
|
+
``payload`` carries only JSON-safe, picklable data: the formula and
|
|
245
|
+
premises travel as :mod:`unicode_logic_kit.fol.serialize` envelopes rather
|
|
246
|
+
than raw ``Node`` objects, so this worker does not depend on ``Node``'s
|
|
247
|
+
own pickle-ability. Deserialises, decides through
|
|
248
|
+
:func:`~unicode_logic_kit.atp.protocol.run_backend` (the SAME
|
|
249
|
+
availability/error contract as any other caller), and returns a plain
|
|
250
|
+
``{"backend", "verdict"}`` dict — itself picklable back to the parent.
|
|
251
|
+
"""
|
|
252
|
+
formula = _fol_deserialize(payload["formula"])
|
|
253
|
+
premises = [_fol_deserialize(p) for p in payload["premises"]]
|
|
254
|
+
name = payload["backend"]
|
|
255
|
+
extra = dict(payload["options"])
|
|
256
|
+
if name == "isabelle":
|
|
257
|
+
extra["logic"] = payload["logic"]
|
|
258
|
+
verdict = run_backend(name, formula, premises, timeout=payload["timeout"], **extra)
|
|
259
|
+
return {"backend": name, "verdict": verdict.to_dict()}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _as_plain_data(value):
|
|
263
|
+
"""``value`` with every read-only mapping inside it as a plain ``dict``.
|
|
264
|
+
|
|
265
|
+
An option travels to a worker process by pickling. A mapping that is not a ``dict``
|
|
266
|
+
need not pickle: :attr:`~unicode_logic_kit.fol.signature.Signature.subsorts` is a
|
|
267
|
+
read-only view (``mappingproxy``), and ``subsorts=sig.subsorts`` is how a caller
|
|
268
|
+
passes it. The copy has the same keys and values, which is all a backend reads.
|
|
269
|
+
"""
|
|
270
|
+
if isinstance(value, Mapping) and type(value) is not dict:
|
|
271
|
+
return {key: _as_plain_data(item) for key, item in value.items()}
|
|
272
|
+
return value
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _run_parallel(formula: Node, premises: Sequence[Node], backends: Sequence[str],
|
|
276
|
+
logic: str, timeout: int, require_agreement: int, n_jobs: int,
|
|
277
|
+
plan: Dict[str, Tuple[dict, Optional[str]]]) -> Verdict:
|
|
278
|
+
"""Race ``backends`` across ``n_jobs`` worker processes.
|
|
279
|
+
|
|
280
|
+
Every backend is submitted up front, each with the options ``plan`` hands it
|
|
281
|
+
(a member that ``plan`` refuses is not submitted; its refusal is listed with
|
|
282
|
+
the verdicts); verdicts are folded into the
|
|
283
|
+
agreement tally in COMPLETION order (``as_completed``), so the winner is
|
|
284
|
+
genuinely whichever finishes first, not submission order. On a win or a
|
|
285
|
+
contradiction the executor is shut down with ``wait=False,
|
|
286
|
+
cancel_futures=True``: futures not yet started are dropped, and futures
|
|
287
|
+
already running are simply abandoned — a running native solver call
|
|
288
|
+
cannot be interrupted from here (see module docstring), so the orphaned
|
|
289
|
+
worker process just finishes on its own and exits.
|
|
290
|
+
"""
|
|
291
|
+
formula_env = _fol_serialize(formula)
|
|
292
|
+
premise_envs = [_fol_serialize(p) for p in premises]
|
|
293
|
+
|
|
294
|
+
agreeing: Dict[str, List[Verdict]] = {}
|
|
295
|
+
verdicts: List[Verdict] = []
|
|
296
|
+
for name in backends:
|
|
297
|
+
refusal = plan[name][1]
|
|
298
|
+
if refusal is not None:
|
|
299
|
+
verdicts.append(_refused_verdict(name, logic, refusal))
|
|
300
|
+
executor = ProcessPoolExecutor(max_workers=n_jobs)
|
|
301
|
+
try:
|
|
302
|
+
future_to_name = {}
|
|
303
|
+
for name in backends:
|
|
304
|
+
if plan[name][1] is not None:
|
|
305
|
+
continue
|
|
306
|
+
payload = {
|
|
307
|
+
"backend": name,
|
|
308
|
+
"formula": formula_env,
|
|
309
|
+
"premises": premise_envs,
|
|
310
|
+
"logic": logic,
|
|
311
|
+
"timeout": timeout,
|
|
312
|
+
"options": {key: _as_plain_data(value) for key, value in plan[name][0].items()},
|
|
313
|
+
}
|
|
314
|
+
future_to_name[executor.submit(_worker_decide, payload)] = name
|
|
315
|
+
|
|
316
|
+
for future in as_completed(future_to_name):
|
|
317
|
+
name = future_to_name[future]
|
|
318
|
+
try:
|
|
319
|
+
result = future.result()
|
|
320
|
+
except (ValueError, BackendUnavailable):
|
|
321
|
+
# A member refused the CALL (an unknown frame, a premise_names list of the
|
|
322
|
+
# wrong length). run_backend keeps that loud, and so do the sequential path
|
|
323
|
+
# and api.prove: it is raised here too, not recorded as a failure of the
|
|
324
|
+
# infrastructure.
|
|
325
|
+
raise
|
|
326
|
+
except Exception as exc: # worker crash (e.g. broken pool) -> recorded
|
|
327
|
+
verdict = _crash_verdict(name, logic, exc)
|
|
328
|
+
else:
|
|
329
|
+
try:
|
|
330
|
+
verdict = _verdict_from_dict(result["verdict"])
|
|
331
|
+
except Exception as exc:
|
|
332
|
+
verdict = _crash_verdict(name, logic, exc)
|
|
333
|
+
verdicts.append(verdict)
|
|
334
|
+
if verdict.is_definitive:
|
|
335
|
+
try:
|
|
336
|
+
won = _feed(agreeing, verdict, require_agreement)
|
|
337
|
+
except _Contradiction as exc2:
|
|
338
|
+
return _contradiction_verdict(logic, exc2.proved, exc2.refuted)
|
|
339
|
+
if won is not None:
|
|
340
|
+
return won
|
|
341
|
+
return _unknown_verdict(logic, verdicts)
|
|
342
|
+
finally:
|
|
343
|
+
executor.shutdown(wait=False, cancel_futures=True)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# ---------------------------------------------------------------------------
|
|
347
|
+
# Public entry point
|
|
348
|
+
# ---------------------------------------------------------------------------
|
|
349
|
+
|
|
350
|
+
def portfolio_prove(formula: Node, premises: Sequence[Node] = (), *,
|
|
351
|
+
backends: Sequence[str], logic: str = "fol",
|
|
352
|
+
timeout: int = 10000, require_agreement: int = 1,
|
|
353
|
+
jobs: Optional[int] = None, signature=None,
|
|
354
|
+
**options) -> Verdict:
|
|
355
|
+
"""Decide ``premises ⊨ formula`` by racing ``backends`` concurrently.
|
|
356
|
+
|
|
357
|
+
Unlike :func:`unicode_logic_kit.api.prove`, this is an explicit-opt-in
|
|
358
|
+
tool: there is no default backend list, so a caller always names exactly
|
|
359
|
+
which routes it wants raced against each other.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
formula: the goal to decide. A :class:`~unicode_logic_kit.logic.Sentence` in
|
|
363
|
+
classical first-order logic is accepted as :func:`unicode_logic_kit.api.prove`
|
|
364
|
+
accepts it: its side axioms are added to the premises.
|
|
365
|
+
premises: local premises; folded into ``(∧ premises) → formula``
|
|
366
|
+
exactly as every other backend entry point does. A Sentence among
|
|
367
|
+
them brings its side axioms, as for ``formula``.
|
|
368
|
+
backends: REQUIRED, non-empty. Every name is validated (unknown name
|
|
369
|
+
→ ``ValueError``, known-but-unavailable → ``BackendUnavailable``)
|
|
370
|
+
and checked against ``logic`` (mismatched → ``ValueError``)
|
|
371
|
+
BEFORE anything is spawned — never a partial run that discovers
|
|
372
|
+
a bad name mid-flight.
|
|
373
|
+
logic: the logic every backend in ``backends`` must support
|
|
374
|
+
(``"fol"``, ``"modal"``, ...). No auto-detection here (contrast
|
|
375
|
+
``prove(logic="auto")``) — a portfolio's whole point is a
|
|
376
|
+
caller-chosen, homogeneous backend set.
|
|
377
|
+
timeout: forwarded to every backend, in milliseconds.
|
|
378
|
+
require_agreement: how many backends must report the SAME definitive
|
|
379
|
+
status before their verdict is returned (default 1: first
|
|
380
|
+
definitive answer wins). The returned ``Verdict.agreement``
|
|
381
|
+
lists exactly those backend names.
|
|
382
|
+
jobs: worker processes. ``None`` (default) picks
|
|
383
|
+
``min(len(backends), 8)``; any explicit value is clamped to
|
|
384
|
+
``min(jobs, 8)`` (project-wide cap) and floored at 1. ``jobs==1``
|
|
385
|
+
runs sequentially in THIS process — no
|
|
386
|
+
``ProcessPoolExecutor`` — which is also the only way to race a
|
|
387
|
+
backend registered ad hoc in the caller's own process (a spawned
|
|
388
|
+
worker cannot import a name that only exists there).
|
|
389
|
+
signature: a :class:`~unicode_logic_kit.fol.signature.Signature`, read
|
|
390
|
+
exactly as :func:`unicode_logic_kit.api.prove` reads it: the sentences
|
|
391
|
+
:func:`~unicode_logic_kit.fol.signature_axioms` returns are added to the
|
|
392
|
+
premises of every member, and a premise index that comes back
|
|
393
|
+
(``relevant_premises``, a Z3 core) stays an index into ``premises``.
|
|
394
|
+
``premise_names=`` names the caller's own premises only; the sentences the
|
|
395
|
+
signature adds are named for the writer. Classical first-order routes only.
|
|
396
|
+
**options: planned exactly as :func:`unicode_logic_kit.api.prove` plans
|
|
397
|
+
them (:func:`~unicode_logic_kit.atp.protocol.plan_options`). A member is
|
|
398
|
+
handed only the options it reads. An option that NO member reads is a
|
|
399
|
+
``ValueError`` naming it, raised before anything runs: it would
|
|
400
|
+
otherwise be ignored and the answer given to a question the caller
|
|
401
|
+
did not ask. A member that does not read an option that changes the
|
|
402
|
+
question (a modal ``frame=``, a ``subsorts=`` edge, ``bridges=``)
|
|
403
|
+
while another member does is NOT run: it is listed in the collective
|
|
404
|
+
verdict's ``detail`` as ``unknown/unsupported`` with the option
|
|
405
|
+
named, and it never casts a vote. An option that only bounds a search
|
|
406
|
+
or says where a binary lives (``max_steps=``, ``use_wsl=``) is simply
|
|
407
|
+
not handed to a member that has no use for it.
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
The winning ``Verdict`` on agreement; a ``Verdict(status="error",
|
|
411
|
+
reason="infra", ...)`` naming both sides if two backends disagree
|
|
412
|
+
(PROVED vs REFUTED — a soundness alarm, never silently resolved);
|
|
413
|
+
otherwise a collective ``Verdict(status="unknown", backend=
|
|
414
|
+
"portfolio", ...)`` whose ``detail`` summarises every backend's own
|
|
415
|
+
answer.
|
|
416
|
+
|
|
417
|
+
Raises:
|
|
418
|
+
ValueError: ``backends`` is empty, ``require_agreement < 1``, a
|
|
419
|
+
backend name is unregistered, a named backend does not
|
|
420
|
+
support ``logic``, an option is read by no member, a Sentence is
|
|
421
|
+
in a logic other than classical first-order logic, ``signature``
|
|
422
|
+
is given for a logic other than ``"fol"``, or a member refuses the
|
|
423
|
+
call itself (an unknown ``frame=``, a ``premise_names=`` list of the
|
|
424
|
+
wrong length) — the same ``ValueError`` for every ``jobs``.
|
|
425
|
+
TypeError: ``signature`` is not a
|
|
426
|
+
:class:`~unicode_logic_kit.fol.signature.Signature`.
|
|
427
|
+
BackendUnavailable: a named backend is registered but its
|
|
428
|
+
prerequisites (binary, install) are missing, asked for the route
|
|
429
|
+
this call's own options select
|
|
430
|
+
(:meth:`~unicode_logic_kit.atp.protocol.ProverBackend.available_for`).
|
|
431
|
+
"""
|
|
432
|
+
if not backends:
|
|
433
|
+
raise ValueError(
|
|
434
|
+
"portfolio_prove: `backends` must be a non-empty explicit list — "
|
|
435
|
+
"the portfolio is opt-in, there is no default chain "
|
|
436
|
+
"(use unicode_logic_kit.api.prove for that).")
|
|
437
|
+
if require_agreement < 1:
|
|
438
|
+
raise ValueError(
|
|
439
|
+
f"portfolio_prove: require_agreement must be >= 1, got {require_agreement!r}")
|
|
440
|
+
|
|
441
|
+
from ..api import ( # lazy: api imports this package
|
|
442
|
+
_name_background, _signature_premises, _unwrap_sentences, _without_background,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
backends = list(backends)
|
|
446
|
+
premises = list(premises)
|
|
447
|
+
given = len(premises) # the caller's own premises
|
|
448
|
+
formula, premises = _unwrap_sentences(formula, premises, "portfolio_prove")
|
|
449
|
+
|
|
450
|
+
for name in backends:
|
|
451
|
+
backend = get_backend(name) # ValueError on unknown name
|
|
452
|
+
if logic not in backend.logics:
|
|
453
|
+
raise ValueError(
|
|
454
|
+
f"portfolio_prove: backend {name!r} does not support logic {logic!r} "
|
|
455
|
+
f"(it handles {sorted(backend.logics)})")
|
|
456
|
+
|
|
457
|
+
if signature is not None:
|
|
458
|
+
premises = [*premises, *_signature_premises("portfolio_prove", signature, logic)]
|
|
459
|
+
# What was appended to the caller's premises (a signature's sentences, the side
|
|
460
|
+
# axioms of a Sentence) is background: the caller's premise_names cover the caller's
|
|
461
|
+
# premises, and the background is named for the writer, exactly as api.prove does.
|
|
462
|
+
options = _name_background(options, given, len(premises), "portfolio_prove")
|
|
463
|
+
|
|
464
|
+
# The options of the call are planned as api.prove plans them: a member is
|
|
465
|
+
# handed what it reads, an option nobody reads is a ValueError, and a member
|
|
466
|
+
# that cannot read an option that changes the question is not run at all.
|
|
467
|
+
plan = plan_options("portfolio_prove", backends, logic, options)
|
|
468
|
+
|
|
469
|
+
for name in backends:
|
|
470
|
+
backend = get_backend(name)
|
|
471
|
+
if not _available_for(backend, plan[name][0]): # the route this call selects
|
|
472
|
+
raise BackendUnavailable(
|
|
473
|
+
f"{name}: backend is not available on this machine "
|
|
474
|
+
f"(external={backend.external}) — install it or drop it from `backends`.")
|
|
475
|
+
|
|
476
|
+
if jobs is None:
|
|
477
|
+
n_jobs = min(len(backends), _MAX_JOBS)
|
|
478
|
+
else:
|
|
479
|
+
n_jobs = max(1, min(int(jobs), _MAX_JOBS))
|
|
480
|
+
|
|
481
|
+
if n_jobs == 1:
|
|
482
|
+
verdict = _run_sequential(formula, premises, backends, logic, timeout,
|
|
483
|
+
require_agreement, plan)
|
|
484
|
+
else:
|
|
485
|
+
verdict = _run_parallel(formula, premises, backends, logic, timeout,
|
|
486
|
+
require_agreement, n_jobs, plan)
|
|
487
|
+
if len(premises) > given:
|
|
488
|
+
verdict = _without_background(verdict, given)
|
|
489
|
+
return verdict
|