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.
Files changed (237) hide show
  1. unicode_logic_kit/__init__.py +385 -0
  2. unicode_logic_kit/__main__.py +520 -0
  3. unicode_logic_kit/_deadline.py +219 -0
  4. unicode_logic_kit/ace/__init__.py +126 -0
  5. unicode_logic_kit/ace/_align.py +135 -0
  6. unicode_logic_kit/ace/chem_lexicon.py +128 -0
  7. unicode_logic_kit/ace/drs_reader.py +570 -0
  8. unicode_logic_kit/ace/mapping.py +666 -0
  9. unicode_logic_kit/ace/reverse_modal.py +138 -0
  10. unicode_logic_kit/ace/runner.py +551 -0
  11. unicode_logic_kit/ace/translate.py +452 -0
  12. unicode_logic_kit/ace/verbalize.py +1070 -0
  13. unicode_logic_kit/api.py +1284 -0
  14. unicode_logic_kit/atp/__init__.py +177 -0
  15. unicode_logic_kit/atp/_ascii_names.py +113 -0
  16. unicode_logic_kit/atp/_html.py +72 -0
  17. unicode_logic_kit/atp/_substructural_input.py +228 -0
  18. unicode_logic_kit/atp/_tff_problem.py +715 -0
  19. unicode_logic_kit/atp/_tptp_problem.py +1111 -0
  20. unicode_logic_kit/atp/_writer_support.py +289 -0
  21. unicode_logic_kit/atp/clingo_backend.py +1180 -0
  22. unicode_logic_kit/atp/cvc5_backend.py +1385 -0
  23. unicode_logic_kit/atp/eprover_backend.py +732 -0
  24. unicode_logic_kit/atp/finite_domain.py +1055 -0
  25. unicode_logic_kit/atp/fitch.py +1547 -0
  26. unicode_logic_kit/atp/fitch_search.py +551 -0
  27. unicode_logic_kit/atp/hets_backend.py +339 -0
  28. unicode_logic_kit/atp/hybrid_down.py +120 -0
  29. unicode_logic_kit/atp/incremental.py +250 -0
  30. unicode_logic_kit/atp/kripke_enum.py +741 -0
  31. unicode_logic_kit/atp/lambek.py +436 -0
  32. unicode_logic_kit/atp/leo3_backend.py +332 -0
  33. unicode_logic_kit/atp/linear.py +738 -0
  34. unicode_logic_kit/atp/lj.py +705 -0
  35. unicode_logic_kit/atp/logic_backends.py +566 -0
  36. unicode_logic_kit/atp/ltl_tableau.py +1084 -0
  37. unicode_logic_kit/atp/minizinc_backend.py +1402 -0
  38. unicode_logic_kit/atp/modal_tableau.py +1382 -0
  39. unicode_logic_kit/atp/nanocop_backend.py +410 -0
  40. unicode_logic_kit/atp/portfolio.py +489 -0
  41. unicode_logic_kit/atp/protocol.py +1803 -0
  42. unicode_logic_kit/atp/prover9_entailment.py +1153 -0
  43. unicode_logic_kit/atp/resolution.py +1376 -0
  44. unicode_logic_kit/atp/resolution_check.py +1114 -0
  45. unicode_logic_kit/atp/sequent.py +1050 -0
  46. unicode_logic_kit/atp/tableau.py +921 -0
  47. unicode_logic_kit/atp/tableau_check.py +543 -0
  48. unicode_logic_kit/atp/tptp_ncl.py +811 -0
  49. unicode_logic_kit/atp/tptp_tff.py +1546 -0
  50. unicode_logic_kit/atp/tstp.py +1333 -0
  51. unicode_logic_kit/atp/tstp_check.py +1096 -0
  52. unicode_logic_kit/atp/twee_backend.py +236 -0
  53. unicode_logic_kit/atp/twee_check.py +711 -0
  54. unicode_logic_kit/atp/twee_entailment.py +953 -0
  55. unicode_logic_kit/atp/vampire_entailment.py +540 -0
  56. unicode_logic_kit/atp/z3_arith.py +470 -0
  57. unicode_logic_kit/atp/z3_equivalence.py +36 -0
  58. unicode_logic_kit/atp/z3_fuzzy.py +362 -0
  59. unicode_logic_kit/atp/z3_input.py +500 -0
  60. unicode_logic_kit/atp/z3_models.py +208 -0
  61. unicode_logic_kit/chem/__init__.py +88 -0
  62. unicode_logic_kit/chem/_naming.py +284 -0
  63. unicode_logic_kit/chem/cache.py +185 -0
  64. unicode_logic_kit/chem/interop.py +244 -0
  65. unicode_logic_kit/chem/mol.py +525 -0
  66. unicode_logic_kit/chem/signature.py +112 -0
  67. unicode_logic_kit/comorphism.py +497 -0
  68. unicode_logic_kit/dl/__init__.py +384 -0
  69. unicode_logic_kit/dl/classification.py +227 -0
  70. unicode_logic_kit/dl/concepts.py +632 -0
  71. unicode_logic_kit/dl/datatypes.py +818 -0
  72. unicode_logic_kit/dl/owl_functional.py +2433 -0
  73. unicode_logic_kit/dl/owl_manchester.py +1637 -0
  74. unicode_logic_kit/dl/owl_reasoner.py +790 -0
  75. unicode_logic_kit/dl/parser.py +391 -0
  76. unicode_logic_kit/dl/tableau.py +4048 -0
  77. unicode_logic_kit/dl/translate.py +2704 -0
  78. unicode_logic_kit/drt/__init__.py +94 -0
  79. unicode_logic_kit/drt/export.py +179 -0
  80. unicode_logic_kit/drt/nodes.py +506 -0
  81. unicode_logic_kit/drt/parser.py +965 -0
  82. unicode_logic_kit/drt/resolve.py +195 -0
  83. unicode_logic_kit/drt/reverse.py +175 -0
  84. unicode_logic_kit/eval/__init__.py +106 -0
  85. unicode_logic_kit/eval/batch.py +382 -0
  86. unicode_logic_kit/eval/canonical.py +663 -0
  87. unicode_logic_kit/eval/chem_batch.py +606 -0
  88. unicode_logic_kit/eval/converses.py +200 -0
  89. unicode_logic_kit/eval/datasets/__init__.py +136 -0
  90. unicode_logic_kit/eval/datasets/_base.py +263 -0
  91. unicode_logic_kit/eval/datasets/_proofwriter_proof.py +422 -0
  92. unicode_logic_kit/eval/datasets/c3po.py +678 -0
  93. unicode_logic_kit/eval/datasets/folio.py +158 -0
  94. unicode_logic_kit/eval/datasets/fracas.py +418 -0
  95. unicode_logic_kit/eval/datasets/groves.py +191 -0
  96. unicode_logic_kit/eval/datasets/logicbench.py +467 -0
  97. unicode_logic_kit/eval/datasets/logicnli.py +303 -0
  98. unicode_logic_kit/eval/datasets/malls.py +133 -0
  99. unicode_logic_kit/eval/datasets/pfolio.py +594 -0
  100. unicode_logic_kit/eval/datasets/pmb.py +242 -0
  101. unicode_logic_kit/eval/datasets/prontoqa.py +611 -0
  102. unicode_logic_kit/eval/datasets/proofwriter.py +1431 -0
  103. unicode_logic_kit/eval/datasets/proverqa.py +674 -0
  104. unicode_logic_kit/eval/datasets/willow.py +478 -0
  105. unicode_logic_kit/eval/equivalence.py +466 -0
  106. unicode_logic_kit/eval/exercise_gen.py +533 -0
  107. unicode_logic_kit/eval/explain.py +791 -0
  108. unicode_logic_kit/eval/generality.py +750 -0
  109. unicode_logic_kit/eval/metric_hf.py +458 -0
  110. unicode_logic_kit/eval/predicate_match.py +343 -0
  111. unicode_logic_kit/eval/theory_check.py +1170 -0
  112. unicode_logic_kit/eval/validate.py +306 -0
  113. unicode_logic_kit/fol/__init__.py +177 -0
  114. unicode_logic_kit/fol/_atom_keys.py +510 -0
  115. unicode_logic_kit/fol/_fol_nodes.py +3586 -0
  116. unicode_logic_kit/fol/_free_parameters.py +105 -0
  117. unicode_logic_kit/fol/_ho_nodes.py +448 -0
  118. unicode_logic_kit/fol/_hybrid_nodes.py +308 -0
  119. unicode_logic_kit/fol/_identifiers.py +1091 -0
  120. unicode_logic_kit/fol/_lambek_nodes.py +112 -0
  121. unicode_logic_kit/fol/_linear_nodes.py +352 -0
  122. unicode_logic_kit/fol/_modal_nodes.py +1467 -0
  123. unicode_logic_kit/fol/_msfl_nodes.py +2196 -0
  124. unicode_logic_kit/fol/_numeral_symbols.py +231 -0
  125. unicode_logic_kit/fol/_so_nodes.py +200 -0
  126. unicode_logic_kit/fol/_symbol_names.py +81 -0
  127. unicode_logic_kit/fol/_team_nodes.py +181 -0
  128. unicode_logic_kit/fol/_tptp_symbols.py +551 -0
  129. unicode_logic_kit/fol/_truth_constants.py +117 -0
  130. unicode_logic_kit/fol/casl_export.py +1135 -0
  131. unicode_logic_kit/fol/casl_import.py +929 -0
  132. unicode_logic_kit/fol/derivation.py +367 -0
  133. unicode_logic_kit/fol/dialect_detect.py +70 -0
  134. unicode_logic_kit/fol/dialect_repair.py +537 -0
  135. unicode_logic_kit/fol/frames.py +637 -0
  136. unicode_logic_kit/fol/grammars/terminals.lark +31 -0
  137. unicode_logic_kit/fol/lambda_tools.py +297 -0
  138. unicode_logic_kit/fol/latex_input.py +429 -0
  139. unicode_logic_kit/fol/modal_translation.py +944 -0
  140. unicode_logic_kit/fol/msflparser.py +1033 -0
  141. unicode_logic_kit/fol/naming.py +422 -0
  142. unicode_logic_kit/fol/nodes.py +241 -0
  143. unicode_logic_kit/fol/normalforms.py +492 -0
  144. unicode_logic_kit/fol/pal.py +287 -0
  145. unicode_logic_kit/fol/prolog_export.py +566 -0
  146. unicode_logic_kit/fol/prolog_input.py +505 -0
  147. unicode_logic_kit/fol/prover9_input.py +1325 -0
  148. unicode_logic_kit/fol/qml.py +1760 -0
  149. unicode_logic_kit/fol/qmltp_input.py +525 -0
  150. unicode_logic_kit/fol/sanitize.py +221 -0
  151. unicode_logic_kit/fol/serialize.py +79 -0
  152. unicode_logic_kit/fol/signature.py +1290 -0
  153. unicode_logic_kit/fol/simplify_check.py +544 -0
  154. unicode_logic_kit/fol/spans.py +594 -0
  155. unicode_logic_kit/fol/tptp_input.py +1503 -0
  156. unicode_logic_kit/fol/tptp_repair.py +941 -0
  157. unicode_logic_kit/fol/unification.py +157 -0
  158. unicode_logic_kit/fol/verbalize.py +263 -0
  159. unicode_logic_kit/hets/__init__.py +163 -0
  160. unicode_logic_kit/hets/bridge.py +142 -0
  161. unicode_logic_kit/hets/client.py +748 -0
  162. unicode_logic_kit/hets/docker.py +420 -0
  163. unicode_logic_kit/hets/dol.py +712 -0
  164. unicode_logic_kit/hets/haskell_json.py +355 -0
  165. unicode_logic_kit/hets/owl_backend.py +794 -0
  166. unicode_logic_kit/hets/owl_cli.py +598 -0
  167. unicode_logic_kit/hets/symbols.py +512 -0
  168. unicode_logic_kit/hol/__init__.py +140 -0
  169. unicode_logic_kit/hol/_ho_common.py +323 -0
  170. unicode_logic_kit/hol/_isabelle_binders.py +125 -0
  171. unicode_logic_kit/hol/classical.py +812 -0
  172. unicode_logic_kit/hol/deepshallow/__init__.py +45 -0
  173. unicode_logic_kit/hol/deepshallow/_common.py +177 -0
  174. unicode_logic_kit/hol/deepshallow/conditional.py +225 -0
  175. unicode_logic_kit/hol/deepshallow/intuitionistic.py +181 -0
  176. unicode_logic_kit/hol/deepshallow/modal.py +217 -0
  177. unicode_logic_kit/hol/deepshallow/qml.py +406 -0
  178. unicode_logic_kit/hol/deepshallow/relevant.py +206 -0
  179. unicode_logic_kit/hol/free.py +753 -0
  180. unicode_logic_kit/hol/goedel.py +336 -0
  181. unicode_logic_kit/hol/ho_modal.py +1743 -0
  182. unicode_logic_kit/hol/intuitionistic.py +403 -0
  183. unicode_logic_kit/hol/isabelle_conditional.py +593 -0
  184. unicode_logic_kit/hol/isabelle_modal.py +1908 -0
  185. unicode_logic_kit/hol/isabelle_relevant.py +412 -0
  186. unicode_logic_kit/hol/isabelle_runner.py +1147 -0
  187. unicode_logic_kit/hol/isabelle_substructural.py +884 -0
  188. unicode_logic_kit/hol/lean.py +1018 -0
  189. unicode_logic_kit/hol/manyvalued.py +921 -0
  190. unicode_logic_kit/hol/secondorder.py +687 -0
  191. unicode_logic_kit/hol/thf_modal.py +941 -0
  192. unicode_logic_kit/hol/thirdorder.py +397 -0
  193. unicode_logic_kit/ilp/__init__.py +89 -0
  194. unicode_logic_kit/ilp/readback.py +389 -0
  195. unicode_logic_kit/ilp/separation.py +153 -0
  196. unicode_logic_kit/ilp/task.py +730 -0
  197. unicode_logic_kit/logic.py +163 -0
  198. unicode_logic_kit/mcp/__init__.py +28 -0
  199. unicode_logic_kit/mcp/__main__.py +5 -0
  200. unicode_logic_kit/mcp/chem_tools.py +1031 -0
  201. unicode_logic_kit/mcp/server.py +2453 -0
  202. unicode_logic_kit/mcp/syntax_spec.py +681 -0
  203. unicode_logic_kit/prob/__init__.py +53 -0
  204. unicode_logic_kit/prob/_bdd.py +225 -0
  205. unicode_logic_kit/prob/_column_gen.py +668 -0
  206. unicode_logic_kit/prob/distribution.py +686 -0
  207. unicode_logic_kit/prob/nilsson.py +470 -0
  208. unicode_logic_kit/py.typed +0 -0
  209. unicode_logic_kit/semantics/__init__.py +137 -0
  210. unicode_logic_kit/semantics/_modal_reject.py +156 -0
  211. unicode_logic_kit/semantics/action_models.py +466 -0
  212. unicode_logic_kit/semantics/asp_models.py +1200 -0
  213. unicode_logic_kit/semantics/conditional.py +580 -0
  214. unicode_logic_kit/semantics/dynamic_epistemic.py +95 -0
  215. unicode_logic_kit/semantics/free_logic.py +913 -0
  216. unicode_logic_kit/semantics/fuzzy.py +384 -0
  217. unicode_logic_kit/semantics/fuzzy_kripke.py +442 -0
  218. unicode_logic_kit/semantics/intuitionistic.py +581 -0
  219. unicode_logic_kit/semantics/kripke.py +1139 -0
  220. unicode_logic_kit/semantics/manyvalued.py +580 -0
  221. unicode_logic_kit/semantics/matrix.py +342 -0
  222. unicode_logic_kit/semantics/model_eval.py +1135 -0
  223. unicode_logic_kit/semantics/modelfinder.py +1036 -0
  224. unicode_logic_kit/semantics/nonmonotonic.py +372 -0
  225. unicode_logic_kit/semantics/relevant.py +331 -0
  226. unicode_logic_kit/semantics/secondorder.py +657 -0
  227. unicode_logic_kit/semantics/structures.py +352 -0
  228. unicode_logic_kit/semantics/tarski.py +975 -0
  229. unicode_logic_kit/semantics/team.py +315 -0
  230. unicode_logic_kit/semantics/team_translation.py +416 -0
  231. unicode_logic_kit/semantics/thirdorder.py +358 -0
  232. unicode_logic_kit/semantics/tnorm.py +85 -0
  233. unicode_logic_kit/semantics/truthtable.py +201 -0
  234. unicode_logic_kit-0.31.0.dist-info/METADATA +333 -0
  235. unicode_logic_kit-0.31.0.dist-info/RECORD +237 -0
  236. unicode_logic_kit-0.31.0.dist-info/WHEEL +4 -0
  237. 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