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,382 @@
|
|
|
1
|
+
"""Batch/cache infrastructure for mass evaluation (10k+ formulas).
|
|
2
|
+
|
|
3
|
+
:func:`batch_decide` runs :func:`unicode_logic_kit.api.prove` over many tasks at
|
|
4
|
+
once, adding three things a single ``prove()`` call does not need but a
|
|
5
|
+
10k-formula evaluation run cannot do without:
|
|
6
|
+
|
|
7
|
+
* **Never abort the batch.** A task is a raw ``{"id", "formula", "premises"}``
|
|
8
|
+
dict; ``formula``/each ``premises`` entry may be a parsed
|
|
9
|
+
:class:`~unicode_logic_kit.fol.nodes.Node` or raw text. Text goes through
|
|
10
|
+
:func:`unicode_logic_kit.api.parse_any`. A parse failure becomes an
|
|
11
|
+
``"error"``/``"parse"`` result for THAT task — it is recorded, never raised,
|
|
12
|
+
and never stops the remaining tasks from running. (Backend-level
|
|
13
|
+
configuration errors — an unknown backend name, a backend/logic mismatch —
|
|
14
|
+
are a different thing: those are caller bugs, not per-formula noise, and
|
|
15
|
+
still raise, exactly as :func:`~unicode_logic_kit.api.prove` does.)
|
|
16
|
+
* **Content-addressed caching.** With ``cache_dir`` set, every task's decision
|
|
17
|
+
is looked up under a key derived from everything that could change the
|
|
18
|
+
answer: the formula, the premises, the backend selection, the logic, the
|
|
19
|
+
timeout, the extra options, and each effective backend's own reported
|
|
20
|
+
``solver_version()`` (K1 — see :func:`_cache_key`'s docstring) — all of
|
|
21
|
+
it, not just the formula, because the same formula decided with a
|
|
22
|
+
different backend chain, a tighter timeout, or a different external tool
|
|
23
|
+
version is not the same cache entry. A hit returns the stored
|
|
24
|
+
:class:`~unicode_logic_kit.atp.protocol.Verdict` dict WITHOUT calling any
|
|
25
|
+
backend. Only definitive/UNKNOWN verdicts are cached — an ``"error"``
|
|
26
|
+
verdict (infra failure, e.g. a subprocess crash) is never cached, so a
|
|
27
|
+
transient failure does not poison the cache for a formula that would
|
|
28
|
+
otherwise decide cleanly on retry.
|
|
29
|
+
* **Process-based parallelism.** ``jobs > 1`` fans the (post-cache, post-parse)
|
|
30
|
+
work out over a ``ProcessPoolExecutor``, never threads: Z3's ``Solver`` and
|
|
31
|
+
the kit's other backends share process-global state (Z3 contexts in
|
|
32
|
+
particular are documented as not thread-safe), so THREADS would risk
|
|
33
|
+
cross-task interference invisible in a small test but real at 10k-formula
|
|
34
|
+
scale. Processes are the correct isolation unit. Per the project's
|
|
35
|
+
parallelism cap, ``jobs`` is clamped to ``min(jobs, 8)`` regardless of what
|
|
36
|
+
is requested. ``jobs=1`` (the default) never touches
|
|
37
|
+
``ProcessPoolExecutor`` — no spawn overhead for the common single-worker
|
|
38
|
+
case.
|
|
39
|
+
|
|
40
|
+
Result shape: ``batch_decide`` returns a ``list[dict]``, one entry per input
|
|
41
|
+
task, in INPUT ORDER (parallel execution does not reorder results) —
|
|
42
|
+
``{"id": ..., "verdict": Verdict.to_dict(), "cached": bool}``. With
|
|
43
|
+
``results_path`` set, the same list is also appended to that path as JSONL
|
|
44
|
+
(one JSON object per line, in the same order).
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
import hashlib
|
|
48
|
+
import json
|
|
49
|
+
import os
|
|
50
|
+
import tempfile
|
|
51
|
+
from typing import Dict, Iterable, List, Optional, Sequence
|
|
52
|
+
|
|
53
|
+
from ..atp.protocol import ERROR, Verdict, default_chain, get_backend
|
|
54
|
+
from ..fol.nodes import Node
|
|
55
|
+
from ..fol.serialize import deserialize as _fol_deserialize
|
|
56
|
+
from ..fol.serialize import serialize as _fol_serialize
|
|
57
|
+
|
|
58
|
+
__all__ = ["batch_decide"]
|
|
59
|
+
|
|
60
|
+
# Project-wide parallelism cap (see memory: "cap all parallelism at 8").
|
|
61
|
+
_MAX_JOBS = 8
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
# Task resolution: Node | str -> Node, never raising
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
def _resolve_one(value) -> "tuple[Optional[Node], Optional[str]]":
|
|
69
|
+
"""Resolve one formula/premise value to a ``Node``, or an error message.
|
|
70
|
+
|
|
71
|
+
Returns ``(node, None)`` on success, ``(None, message)`` on failure.
|
|
72
|
+
Never raises: a parser exception is caught and turned into ``message``.
|
|
73
|
+
"""
|
|
74
|
+
if isinstance(value, Node):
|
|
75
|
+
return value, None
|
|
76
|
+
if isinstance(value, str):
|
|
77
|
+
from .. import api # lazy: avoid import-time cost/cycles
|
|
78
|
+
|
|
79
|
+
parsed = api.parse_any(value)
|
|
80
|
+
if not parsed.ok:
|
|
81
|
+
message = parsed.errors[-1]["message"] if parsed.errors else "unparseable"
|
|
82
|
+
return None, message
|
|
83
|
+
return parsed.formula, None
|
|
84
|
+
return None, f"unsupported formula/premise type: {type(value).__name__}"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _resolve_task(task: dict):
|
|
88
|
+
"""Resolve a task dict to ``(id, formula, premises, error)``.
|
|
89
|
+
|
|
90
|
+
``error`` is ``None`` on success; on failure ``formula``/``premises`` are
|
|
91
|
+
``None`` and ``error`` is a human-readable message (formula error, or
|
|
92
|
+
``"premise <i>: ..."`` for a bad premise).
|
|
93
|
+
"""
|
|
94
|
+
task_id = task["id"]
|
|
95
|
+
formula, err = _resolve_one(task["formula"])
|
|
96
|
+
if err is not None:
|
|
97
|
+
return task_id, None, None, err
|
|
98
|
+
premises: List[Node] = []
|
|
99
|
+
for i, raw in enumerate(task.get("premises") or []):
|
|
100
|
+
node, perr = _resolve_one(raw)
|
|
101
|
+
if perr is not None:
|
|
102
|
+
return task_id, None, None, f"premise {i}: {perr}"
|
|
103
|
+
premises.append(node)
|
|
104
|
+
return task_id, formula, premises, None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _error_result(task_id: str, message: str) -> dict:
|
|
108
|
+
"""Build the per-task result dict for a parse failure (status=error)."""
|
|
109
|
+
verdict = Verdict(ERROR, "batch", reason="parse", detail=message)
|
|
110
|
+
return {"id": task_id, "verdict": verdict.to_dict(), "cached": False}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# Content-addressed cache
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
def _cache_key(formula: Node, premises: Sequence[Node],
|
|
118
|
+
backends: Optional[Sequence[str]], logic: str, timeout: int,
|
|
119
|
+
options: dict) -> str:
|
|
120
|
+
"""sha256 of the canonical JSON of everything that can change the answer.
|
|
121
|
+
|
|
122
|
+
Three properties matter for correctness (all pinned by tests):
|
|
123
|
+
|
|
124
|
+
- The backend list enters the key IN THE CALLER'S ORDER — ``prove``'s
|
|
125
|
+
chain is order-sensitive (the first definitive backend wins), so
|
|
126
|
+
``["z3", "resolution"]`` and ``["resolution", "z3"]`` are different
|
|
127
|
+
queries and must never share a cache entry.
|
|
128
|
+
- ``backends=None`` (the caller wants the default chain) is resolved to
|
|
129
|
+
the CONCRETE chain that would run — ``default_chain`` over the
|
|
130
|
+
detected logic — because the default chain is install-dependent (the
|
|
131
|
+
optional cvc5 extra joins it when importable): a cache populated
|
|
132
|
+
before an install change must not keep answering for the old chain.
|
|
133
|
+
- Each concrete backend NAME in the effective chain (the caller's own
|
|
134
|
+
list, or the resolved default chain above) also contributes its
|
|
135
|
+
OWN ``solver_version()`` (K1: an upgraded/downgraded external tool —
|
|
136
|
+
a new Vampire build, a different HETS image, a bumped ``cvc5`` pip
|
|
137
|
+
package — must invalidate a cache entry the old tool's answer is
|
|
138
|
+
stored under, exactly like the install-dependent default-chain
|
|
139
|
+
resolution above). This is queried HERE, in the parent process,
|
|
140
|
+
once per (cache-missing) task in Pass 1 — BEFORE any
|
|
141
|
+
``ProcessPoolExecutor`` dispatch — so it is each backend's own
|
|
142
|
+
PROCESS-LOCAL memoization (``atp.protocol._binary_version`` for the
|
|
143
|
+
subprocess-spawning backends, the analogous one-shot caches in
|
|
144
|
+
``hets_backend``/``cvc5_backend``) that keeps this cheap across a
|
|
145
|
+
10k-task batch: the FIRST task naming a given backend pays one
|
|
146
|
+
subprocess/HTTP/importlib round trip, every later task naming that
|
|
147
|
+
same backend in the same process is free. Do not "simplify" this
|
|
148
|
+
away by hoisting the lookup out of the per-task call — the
|
|
149
|
+
per-backend memoization already IS the hoisting, at finer grain
|
|
150
|
+
(per binary/server/package, not per whole cache-key computation),
|
|
151
|
+
and doing it per task here is what lets a mid-batch install change
|
|
152
|
+
(rare, but the whole point of the 0.108.0-vs-upgraded-HETS-image
|
|
153
|
+
scenario this item exists for) still show up correctly for every
|
|
154
|
+
task decided after it.
|
|
155
|
+
|
|
156
|
+
``json.dumps(..., sort_keys=True)`` canonicalises dict-key order
|
|
157
|
+
recursively, so ``options``/``solver_versions`` need no manual sorting.
|
|
158
|
+
"""
|
|
159
|
+
if backends is not None:
|
|
160
|
+
effective_backends = list(backends)
|
|
161
|
+
backend_material = effective_backends
|
|
162
|
+
else:
|
|
163
|
+
effective_logic = logic
|
|
164
|
+
if logic == "auto":
|
|
165
|
+
# Mirror api.prove's own routing: modal operators anywhere in
|
|
166
|
+
# the query select the modal chain.
|
|
167
|
+
from ..atp.modal_tableau import has_modal
|
|
168
|
+
effective_logic = ("modal" if has_modal(formula)
|
|
169
|
+
or any(has_modal(p) for p in premises)
|
|
170
|
+
else "fol")
|
|
171
|
+
effective_backends = list(default_chain(effective_logic))
|
|
172
|
+
backend_material = ["default", effective_logic, *effective_backends]
|
|
173
|
+
|
|
174
|
+
solver_versions = {name: get_backend(name).solver_version()
|
|
175
|
+
for name in effective_backends}
|
|
176
|
+
|
|
177
|
+
material = {
|
|
178
|
+
"formula": _fol_serialize(formula),
|
|
179
|
+
"premises": [_fol_serialize(p) for p in premises],
|
|
180
|
+
"backends": backend_material,
|
|
181
|
+
"solver_versions": solver_versions,
|
|
182
|
+
"logic": logic,
|
|
183
|
+
"timeout": timeout,
|
|
184
|
+
"options": options,
|
|
185
|
+
}
|
|
186
|
+
blob = json.dumps(material, sort_keys=True, separators=(",", ":"))
|
|
187
|
+
return hashlib.sha256(blob.encode("utf-8")).hexdigest()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _cache_path(cache_dir: str, key: str) -> str:
|
|
191
|
+
return os.path.join(cache_dir, f"{key}.json")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _cache_read(cache_dir: str, key: str) -> Optional[dict]:
|
|
195
|
+
"""Return the cached verdict dict, or ``None`` on a miss.
|
|
196
|
+
|
|
197
|
+
A corrupt/unreadable cache entry is treated as a miss (never raises) —
|
|
198
|
+
a damaged file must not take down a 10k-task batch.
|
|
199
|
+
"""
|
|
200
|
+
path = _cache_path(cache_dir, key)
|
|
201
|
+
try:
|
|
202
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
203
|
+
return json.load(fh)
|
|
204
|
+
except FileNotFoundError:
|
|
205
|
+
return None
|
|
206
|
+
except (OSError, json.JSONDecodeError):
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _cache_write(cache_dir: str, key: str, verdict_dict: dict) -> None:
|
|
211
|
+
"""Write ``verdict_dict`` under ``key``, atomically (tmp file + replace)."""
|
|
212
|
+
os.makedirs(cache_dir, exist_ok=True)
|
|
213
|
+
fd, tmp_path = tempfile.mkstemp(prefix=".batch-cache-", suffix=".json",
|
|
214
|
+
dir=cache_dir)
|
|
215
|
+
try:
|
|
216
|
+
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
|
217
|
+
json.dump(verdict_dict, fh)
|
|
218
|
+
os.replace(tmp_path, _cache_path(cache_dir, key))
|
|
219
|
+
except BaseException:
|
|
220
|
+
try:
|
|
221
|
+
os.remove(tmp_path)
|
|
222
|
+
except OSError:
|
|
223
|
+
pass
|
|
224
|
+
raise
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
# Deciding: sequential (jobs=1) and per-process worker (jobs>1)
|
|
229
|
+
# ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
def _decide(formula: Node, premises: Sequence[Node],
|
|
232
|
+
backends: Optional[Sequence[str]], logic: str, timeout: int,
|
|
233
|
+
options: dict) -> Verdict:
|
|
234
|
+
"""Call :func:`unicode_logic_kit.api.prove` for one resolved task."""
|
|
235
|
+
from .. import api # lazy: avoid import-time cost/cycles
|
|
236
|
+
|
|
237
|
+
return api.prove(formula, list(premises), logic=logic, backends=backends,
|
|
238
|
+
timeout=timeout, **options)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _worker_decide(payload: dict) -> dict:
|
|
242
|
+
"""``ProcessPoolExecutor`` target — MUST stay module-level for Windows
|
|
243
|
+
spawn (it pickles the target by qualified name, not by closure).
|
|
244
|
+
|
|
245
|
+
``payload`` carries only JSON-safe, picklable data: formulas/premises
|
|
246
|
+
serialised via :mod:`unicode_logic_kit.fol.serialize` rather than raw
|
|
247
|
+
``Node`` objects, so the payload does not depend on ``Node``'s own
|
|
248
|
+
pickle-ability (which the batch layer should not have to assume).
|
|
249
|
+
Deserialises, decides, and returns ``{"id", "verdict"}`` (a plain dict,
|
|
250
|
+
itself picklable back to the parent process).
|
|
251
|
+
"""
|
|
252
|
+
formula = _fol_deserialize(payload["formula"])
|
|
253
|
+
premises = [_fol_deserialize(p) for p in payload["premises"]]
|
|
254
|
+
verdict = _decide(formula, premises, payload["backends"], payload["logic"],
|
|
255
|
+
payload["timeout"], payload["options"])
|
|
256
|
+
return {"id": payload["id"], "verdict": verdict.to_dict()}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# ---------------------------------------------------------------------------
|
|
260
|
+
# batch_decide
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
def batch_decide(tasks: Iterable[dict], *,
|
|
264
|
+
backends: Optional[Sequence[str]] = None,
|
|
265
|
+
logic: str = "auto", timeout: int = 10000, jobs: int = 1,
|
|
266
|
+
cache_dir: Optional[str] = None,
|
|
267
|
+
results_path: Optional[str] = None,
|
|
268
|
+
options: Optional[dict] = None) -> List[dict]:
|
|
269
|
+
"""Decide a batch of entailment tasks, with caching and process fan-out.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
tasks: an iterable of ``{"id": str, "formula": Node | str,
|
|
273
|
+
"premises": optional list[Node | str]}`` dicts. Text values go
|
|
274
|
+
through :func:`unicode_logic_kit.api.parse_any`; a parse failure is
|
|
275
|
+
recorded as an ``"error"``/``"parse"`` result for that task alone
|
|
276
|
+
— it never aborts the batch.
|
|
277
|
+
backends: forwarded to :func:`unicode_logic_kit.api.prove` for every
|
|
278
|
+
task (``None`` = the logic's default chain).
|
|
279
|
+
logic: forwarded to ``prove`` for every task (``"auto"`` routes each
|
|
280
|
+
formula by its own syntax, independently per task).
|
|
281
|
+
timeout: forwarded to ``prove`` for every task, in milliseconds.
|
|
282
|
+
jobs: worker processes for the decide step. Clamped to
|
|
283
|
+
``min(jobs, 8)``. ``jobs<=1`` runs sequentially in this process
|
|
284
|
+
(no ``ProcessPoolExecutor``, no spawn overhead); ``jobs>1`` uses
|
|
285
|
+
one because Z3 (and the kit's other backends) are not
|
|
286
|
+
thread-safe — see the module docstring. CAVEAT for CUSTOM
|
|
287
|
+
backends: worker processes are SPAWNED (fresh interpreters), so
|
|
288
|
+
a backend registered at RUNTIME via ``register_backend`` (e.g.
|
|
289
|
+
inside a script's ``__main__`` block or a test body) does not
|
|
290
|
+
exist in the workers and its tasks come back as unknown-backend
|
|
291
|
+
errors; with ``jobs>1``, register custom backends at the top
|
|
292
|
+
level of an importable module (imported by the workers when they
|
|
293
|
+
re-import this package), or run with ``jobs=1`` — the same
|
|
294
|
+
constraint ``atp.portfolio.portfolio_prove`` documents for its
|
|
295
|
+
own process pool.
|
|
296
|
+
cache_dir: when set, a content-addressed on-disk cache (see
|
|
297
|
+
:func:`_cache_key`). A hit returns the stored verdict WITHOUT
|
|
298
|
+
calling any backend; a miss decides normally and writes the
|
|
299
|
+
result (unless its status is ``"error"``).
|
|
300
|
+
results_path: when set, the full ordered result list is also
|
|
301
|
+
appended to this path as JSONL (one JSON object per line).
|
|
302
|
+
options: extra keyword options forwarded to every backend in every
|
|
303
|
+
task's chain (mirrors ``prove``'s ``**options``); also folded
|
|
304
|
+
into the cache key.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
A ``list[dict]``, one entry per task, in INPUT ORDER:
|
|
308
|
+
``{"id": task_id, "verdict": Verdict.to_dict(), "cached": bool}``.
|
|
309
|
+
|
|
310
|
+
Note:
|
|
311
|
+
A backend-configuration error from ``prove`` itself (unknown backend
|
|
312
|
+
name, a named backend that does not support the detected logic) is a
|
|
313
|
+
caller bug, not per-formula noise, and still raises — exactly as a
|
|
314
|
+
direct ``prove()`` call would. Only PARSE failures are absorbed into
|
|
315
|
+
per-task error results.
|
|
316
|
+
"""
|
|
317
|
+
options = dict(options) if options else {}
|
|
318
|
+
jobs = max(1, min(int(jobs), _MAX_JOBS))
|
|
319
|
+
|
|
320
|
+
resolved = [_resolve_task(task) for task in tasks]
|
|
321
|
+
results: List[Optional[dict]] = [None] * len(resolved)
|
|
322
|
+
|
|
323
|
+
# Pass 1: parse errors -> immediate results; cache lookups -> immediate
|
|
324
|
+
# hits; everything else queued for the decide step (sequential or pool).
|
|
325
|
+
pending_indices: List[int] = []
|
|
326
|
+
pending: List[tuple] = [] # (task_id, formula, premises, cache_key)
|
|
327
|
+
for i, (task_id, formula, premises, err) in enumerate(resolved):
|
|
328
|
+
if err is not None:
|
|
329
|
+
results[i] = _error_result(task_id, err)
|
|
330
|
+
continue
|
|
331
|
+
key = (_cache_key(formula, premises, backends, logic, timeout, options)
|
|
332
|
+
if cache_dir else None)
|
|
333
|
+
if key is not None:
|
|
334
|
+
cached_dict = _cache_read(cache_dir, key)
|
|
335
|
+
if cached_dict is not None:
|
|
336
|
+
results[i] = {"id": task_id, "verdict": cached_dict, "cached": True}
|
|
337
|
+
continue
|
|
338
|
+
pending_indices.append(i)
|
|
339
|
+
pending.append((task_id, formula, premises, key))
|
|
340
|
+
|
|
341
|
+
# Pass 2: decide the cache misses.
|
|
342
|
+
if pending:
|
|
343
|
+
if jobs <= 1:
|
|
344
|
+
verdict_dicts = []
|
|
345
|
+
for task_id, formula, premises, _key in pending:
|
|
346
|
+
verdict = _decide(formula, premises, backends, logic, timeout, options)
|
|
347
|
+
verdict_dicts.append(verdict.to_dict())
|
|
348
|
+
else:
|
|
349
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
350
|
+
|
|
351
|
+
payloads = [
|
|
352
|
+
{
|
|
353
|
+
"id": task_id,
|
|
354
|
+
"formula": _fol_serialize(formula),
|
|
355
|
+
"premises": [_fol_serialize(p) for p in premises],
|
|
356
|
+
"backends": list(backends) if backends is not None else None,
|
|
357
|
+
"logic": logic,
|
|
358
|
+
"timeout": timeout,
|
|
359
|
+
"options": options,
|
|
360
|
+
}
|
|
361
|
+
for task_id, formula, premises, _key in pending
|
|
362
|
+
]
|
|
363
|
+
with ProcessPoolExecutor(max_workers=jobs) as executor:
|
|
364
|
+
# .map preserves input order in its output, so the zip below
|
|
365
|
+
# below stays index-aligned with `pending`.
|
|
366
|
+
worker_results = list(executor.map(_worker_decide, payloads))
|
|
367
|
+
verdict_dicts = [wr["verdict"] for wr in worker_results]
|
|
368
|
+
|
|
369
|
+
for idx, (task_id, _formula, _premises, key), verdict_dict in zip(
|
|
370
|
+
pending_indices, pending, verdict_dicts):
|
|
371
|
+
if cache_dir and key is not None and verdict_dict.get("status") != ERROR:
|
|
372
|
+
_cache_write(cache_dir, key, verdict_dict)
|
|
373
|
+
results[idx] = {"id": task_id, "verdict": verdict_dict, "cached": False}
|
|
374
|
+
|
|
375
|
+
assert all(r is not None for r in results) # every index was filled exactly once
|
|
376
|
+
|
|
377
|
+
if results_path:
|
|
378
|
+
with open(results_path, "a", encoding="utf-8") as fh:
|
|
379
|
+
for r in results:
|
|
380
|
+
fh.write(json.dumps(r) + "\n")
|
|
381
|
+
|
|
382
|
+
return results
|