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,791 @@
|
|
|
1
|
+
"""Plain-English rendering of a countermodel — the "why did this fail" gloss.
|
|
2
|
+
|
|
3
|
+
A countermodel proves *invalidity* by exhibiting a concrete structure, but a
|
|
4
|
+
raw :class:`~unicode_logic_kit.semantics.kripke.KripkeModel`, a raw
|
|
5
|
+
:class:`~unicode_logic_kit.semantics.tarski.Structure`, or a Z3 assignment dict
|
|
6
|
+
is opaque to anyone who is not already reading this codebase.
|
|
7
|
+
:func:`explain_countermodel` turns any of those into a short, deterministic
|
|
8
|
+
English paragraph a human (or an LLM judge) can read directly.
|
|
9
|
+
|
|
10
|
+
Four input shapes are accepted — the ones that actually occur, either built by
|
|
11
|
+
hand/by a semantic search, or produced by the :mod:`unicode_logic_kit.atp.protocol`
|
|
12
|
+
Verdict layer as ``Verdict.countermodel`` / ``CountermodelResult.model``:
|
|
13
|
+
|
|
14
|
+
* :class:`~unicode_logic_kit.semantics.kripke.KripkeModel` — a possible-worlds
|
|
15
|
+
countermodel. Reported: the number of worlds, the edges of every named
|
|
16
|
+
accessibility relation, and the atoms true at each world. If ``formula`` is
|
|
17
|
+
supplied and can actually be evaluated at world 0
|
|
18
|
+
(:func:`~unicode_logic_kit.semantics.kripke.satisfies_modal`), a closing
|
|
19
|
+
sentence names whether it fails there — see the honesty note below.
|
|
20
|
+
* :class:`~unicode_logic_kit.semantics.tarski.Structure` — a finite first-order
|
|
21
|
+
countermodel. Reported: the domain size and its elements, the denotation of
|
|
22
|
+
every constant, and the extension of every predicate and function symbol.
|
|
23
|
+
* a bare ``dict`` with no ``"kind"`` key — a Z3-style assignment
|
|
24
|
+
(``{name: value}``, exactly the shape ``Z3Backend.decide`` puts in
|
|
25
|
+
``Verdict.countermodel["assignment"]``). Reported: every assignment,
|
|
26
|
+
enumerated.
|
|
27
|
+
* a **witness dict** carrying a ``"kind"`` key — the shape the Verdict layer
|
|
28
|
+
actually stores at ``Verdict.countermodel`` / ``CountermodelResult.model``:
|
|
29
|
+
``{"kind": "z3_model", "assignment": {...}}`` (routed to the same Z3
|
|
30
|
+
explanation as the bare-dict case above), or ``{"kind": "kripke" |
|
|
31
|
+
"finite_structure" | "nitpick" | ..., "repr": "<python repr string>"}``.
|
|
32
|
+
The repr-only shapes carry no structured data — only a Python ``repr()`` (or,
|
|
33
|
+
for Isabelle's nitpick, its own textual countermodel) — so they are framed as
|
|
34
|
+
an unparsed witness rather than reformatted as if they were structured; doing
|
|
35
|
+
otherwise would mean inventing structure that was never actually recovered.
|
|
36
|
+
|
|
37
|
+
Honesty note (the same discipline the rest of the kit applies to proof search):
|
|
38
|
+
this function only ever states what it *computed*. The world-0 "fails" sentence
|
|
39
|
+
for a Kripke model is added only when ``satisfies_modal`` actually returns
|
|
40
|
+
``False`` — never inferred. Evaluation failures are treated as "not evaluable"
|
|
41
|
+
and the sentence is silently omitted rather than guessed: a
|
|
42
|
+
:class:`NotImplementedError` (the modal evaluator's own signal for an
|
|
43
|
+
out-of-fragment node, e.g. a quantifier, a fuzzy or lambda node) as well as a
|
|
44
|
+
:class:`ValueError` / :class:`KeyError` (a hybrid nominal with no assignment,
|
|
45
|
+
an unbound variable, a model with no object domains) all count, since none of
|
|
46
|
+
them yield a truth value to report.
|
|
47
|
+
|
|
48
|
+
Determinism: every enumeration (worlds, relation edges, atoms, domain
|
|
49
|
+
elements, constants, predicate/function extensions, Z3 assignments) is sorted
|
|
50
|
+
before rendering — falling back to a ``(type name, str value)`` key when the
|
|
51
|
+
raw values are not mutually orderable (e.g. mixed int/str worlds) — so calling
|
|
52
|
+
this function twice on the same input always returns byte-identical output.
|
|
53
|
+
|
|
54
|
+
Output shape: 2-6 short English sentences, plain text (no Markdown, no
|
|
55
|
+
line-art), joined with single spaces. ``max_sentences`` caps the sentence
|
|
56
|
+
count; content beyond the cap is dropped in the priority order documented on
|
|
57
|
+
each ``_explain_*`` helper below (never reordered, so raising the cap never
|
|
58
|
+
changes which sentences appear first — only how many spill over).
|
|
59
|
+
|
|
60
|
+
:func:`explain_proof` is the same idea for the other side of a
|
|
61
|
+
:class:`~unicode_logic_kit.atp.protocol.Verdict`: a proof of *validity* instead
|
|
62
|
+
of a countermodel of invalidity. See its own docstring for the shapes it
|
|
63
|
+
accepts.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
from itertools import product
|
|
67
|
+
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
68
|
+
|
|
69
|
+
from ..atp.tableau import TableauProof
|
|
70
|
+
from ..atp.tstp import TstpDerivation
|
|
71
|
+
from ..atp.twee_entailment import TweeProof
|
|
72
|
+
from ..fol.nodes import Node
|
|
73
|
+
from ..semantics.kripke import KripkeModel, satisfies_modal
|
|
74
|
+
from ..semantics.tarski import Structure
|
|
75
|
+
|
|
76
|
+
__all__ = ["explain_countermodel", "explain_proof"]
|
|
77
|
+
|
|
78
|
+
#: Exceptions that mean "this evaluation attempt yielded no truth value" —
|
|
79
|
+
#: caught around the optional world-0 formula check so the missing sentence is
|
|
80
|
+
#: skipped instead of the whole explanation crashing. NotImplementedError is
|
|
81
|
+
#: satisfies_modal's own signal for an out-of-fragment node (quantifier, fuzzy,
|
|
82
|
+
#: lambda); ValueError/KeyError cover a hybrid nominal with no assignment, an
|
|
83
|
+
#: unknown quantifier type, or object quantifiers over a model without domains
|
|
84
|
+
#: — all genuine "not evaluable here", never a reason to guess a truth value.
|
|
85
|
+
_NOT_EVALUABLE = (NotImplementedError, ValueError, KeyError)
|
|
86
|
+
|
|
87
|
+
#: Cap on how many tuples a single predicate/function-extension sentence lists
|
|
88
|
+
#: verbatim before it switches to "... and N more" — keeps one enormous
|
|
89
|
+
#: extension from silently eating the whole sentence budget.
|
|
90
|
+
_TUPLE_LIST_LIMIT = 8
|
|
91
|
+
|
|
92
|
+
#: Cap on |domain|**arity before a callable function interpretation is
|
|
93
|
+
#: enumerated by brute-force calling — protects against paying an unbounded
|
|
94
|
+
#: number of calls (and their possible side effects) just to describe one.
|
|
95
|
+
_MAX_FUNCTION_ENUMERATION = 64
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
# Small deterministic-formatting helpers shared by every branch
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
def _s(value: Any) -> str:
|
|
103
|
+
"""Render ``value`` as a single-line string (collapses embedded newlines).
|
|
104
|
+
|
|
105
|
+
Used for every value that ends up inside a sentence, including opaque
|
|
106
|
+
``repr()`` strings from a witness dict, so a stray newline in someone's
|
|
107
|
+
``__repr__`` can never break the "no line-art" output contract.
|
|
108
|
+
"""
|
|
109
|
+
return str(value).replace("\r\n", " ").replace("\n", " ").replace("\r", " ")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _safe_sorted(items: Iterable[Any]) -> List[Any]:
|
|
113
|
+
"""Sort ``items``, falling back to a ``(type name, str)`` key if unorderable.
|
|
114
|
+
|
|
115
|
+
Kripke worlds and structure domain elements are typed as "any hashable
|
|
116
|
+
value", so a mixed-type collection (e.g. some int worlds, some str worlds)
|
|
117
|
+
can raise ``TypeError`` from a direct ``sorted()`` call. The fallback key
|
|
118
|
+
is still total and deterministic — it just does not claim a "natural"
|
|
119
|
+
ordering across incomparable types, only a reproducible one.
|
|
120
|
+
"""
|
|
121
|
+
items = list(items)
|
|
122
|
+
try:
|
|
123
|
+
return sorted(items)
|
|
124
|
+
except TypeError:
|
|
125
|
+
return sorted(items, key=lambda x: (type(x).__name__, str(x)))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _count_word(n: int, noun: str) -> str:
|
|
129
|
+
"""``"1 world"`` / ``"3 worlds"`` — regular pluralisation by appending 's'.
|
|
130
|
+
|
|
131
|
+
Every noun this module counts with (world, edge, individual, assignment)
|
|
132
|
+
pluralises regularly, so no irregular-plural table is needed.
|
|
133
|
+
"""
|
|
134
|
+
return f"{n} {noun}" if n == 1 else f"{n} {noun}s"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _format_tuple(value: Any) -> str:
|
|
138
|
+
"""Render an argument tuple as ``"(a, b)"``, or a bare value as-is."""
|
|
139
|
+
if isinstance(value, tuple):
|
|
140
|
+
return "(" + ", ".join(_s(e) for e in value) + ")"
|
|
141
|
+
return _s(value)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _format_tuple_list(tuples: List[Any], limit: int = _TUPLE_LIST_LIMIT) -> str:
|
|
145
|
+
"""Join formatted tuples with commas, truncating with an "and N more" tail.
|
|
146
|
+
|
|
147
|
+
"entry"/"entries" is an irregular plural, so this is spelled out directly
|
|
148
|
+
rather than routed through :func:`_count_word` (which only handles regular
|
|
149
|
+
``+s`` nouns).
|
|
150
|
+
"""
|
|
151
|
+
shown = tuples[:limit]
|
|
152
|
+
text = ", ".join(_format_tuple(t) for t in shown)
|
|
153
|
+
remaining = len(tuples) - len(shown)
|
|
154
|
+
if remaining == 1:
|
|
155
|
+
text += ", and 1 more entry"
|
|
156
|
+
elif remaining > 1:
|
|
157
|
+
text += f", and {remaining} more entries"
|
|
158
|
+
return text
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
# KripkeModel branch
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
def _explain_kripke(model: KripkeModel, formula: Optional[Node],
|
|
166
|
+
max_sentences: int) -> str:
|
|
167
|
+
"""Explain a possible-worlds countermodel.
|
|
168
|
+
|
|
169
|
+
Sentence priority (highest first, so raising ``max_sentences`` only ever
|
|
170
|
+
reveals more, never reorders what is already shown):
|
|
171
|
+
|
|
172
|
+
1. World count.
|
|
173
|
+
2. One sentence per named relation family with at least one edge (sorted
|
|
174
|
+
by relation name), or a single "no accessibility edges" sentence if
|
|
175
|
+
every relation is empty.
|
|
176
|
+
3. One sentence per world (sorted) naming the atoms true there.
|
|
177
|
+
4. If ``formula`` is given and ``satisfies_modal(formula, model, 0)``
|
|
178
|
+
actually evaluates to ``False``, the fixed sentence
|
|
179
|
+
"At world 0 the formula fails." — omitted (not guessed) whenever the
|
|
180
|
+
evaluation is not evaluable at all (see ``_NOT_EVALUABLE``) or returns
|
|
181
|
+
``True``.
|
|
182
|
+
"""
|
|
183
|
+
worlds = _safe_sorted(model.worlds)
|
|
184
|
+
sentences: List[str] = [
|
|
185
|
+
f"The countermodel has {_count_word(len(worlds), 'possible world')}."
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
relation_sentences: List[str] = []
|
|
189
|
+
for name in sorted(model.relations):
|
|
190
|
+
edges = _safe_sorted(model.relations[name])
|
|
191
|
+
if not edges:
|
|
192
|
+
continue
|
|
193
|
+
edges_str = ", ".join(f"{_s(a)} → {_s(b)}" for a, b in edges)
|
|
194
|
+
relation_sentences.append(
|
|
195
|
+
f'The "{name}" relation has {_count_word(len(edges), "edge")}: {edges_str}.'
|
|
196
|
+
)
|
|
197
|
+
if not relation_sentences:
|
|
198
|
+
relation_sentences = ["There are no accessibility edges between worlds."]
|
|
199
|
+
sentences += relation_sentences
|
|
200
|
+
|
|
201
|
+
for w in worlds:
|
|
202
|
+
atoms = sorted(model.atoms_true_at(w))
|
|
203
|
+
if atoms:
|
|
204
|
+
noun = "atom" if len(atoms) == 1 else "atoms"
|
|
205
|
+
verb = "is" if len(atoms) == 1 else "are"
|
|
206
|
+
sentences.append(
|
|
207
|
+
f"At world {_s(w)}, the {noun} {', '.join(atoms)} {verb} true."
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
sentences.append(f"At world {_s(w)}, no atoms are true.")
|
|
211
|
+
|
|
212
|
+
if formula is not None:
|
|
213
|
+
try:
|
|
214
|
+
value = satisfies_modal(formula, model, 0)
|
|
215
|
+
except _NOT_EVALUABLE:
|
|
216
|
+
value = None
|
|
217
|
+
if value is False:
|
|
218
|
+
sentences.append("At world 0 the formula fails.")
|
|
219
|
+
|
|
220
|
+
return " ".join(sentences[:max_sentences])
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# ---------------------------------------------------------------------------
|
|
224
|
+
# Structure branch
|
|
225
|
+
# ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
def _resolve_function_mapping(interp: Any, domain: List[Any], arity: int
|
|
228
|
+
) -> Optional[Dict[Tuple[Any, ...], Any]]:
|
|
229
|
+
"""Return ``{arg_tuple: value}`` for a function interpretation, or ``None``.
|
|
230
|
+
|
|
231
|
+
``None`` means "not enumerated" — either the interpretation is a callable
|
|
232
|
+
over a domain too large to brute-force (see
|
|
233
|
+
``_MAX_FUNCTION_ENUMERATION``), or calling it raised. Both are reported
|
|
234
|
+
honestly as "not enumerated" rather than silently omitted or guessed.
|
|
235
|
+
"""
|
|
236
|
+
if isinstance(interp, dict):
|
|
237
|
+
return dict(interp)
|
|
238
|
+
if callable(interp):
|
|
239
|
+
if len(domain) ** arity > _MAX_FUNCTION_ENUMERATION:
|
|
240
|
+
return None
|
|
241
|
+
mapping: Dict[Tuple[Any, ...], Any] = {}
|
|
242
|
+
try:
|
|
243
|
+
for args in product(domain, repeat=arity):
|
|
244
|
+
mapping[args] = interp(*args)
|
|
245
|
+
except Exception:
|
|
246
|
+
return None
|
|
247
|
+
return mapping
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _explain_structure(structure: Structure, max_sentences: int) -> str:
|
|
252
|
+
"""Explain a finite first-order countermodel.
|
|
253
|
+
|
|
254
|
+
Sentence priority: domain size and elements; the denotation of every
|
|
255
|
+
constant (one combined sentence); then one sentence per predicate and one
|
|
256
|
+
per function (both sorted by ``(name, arity)``), each stating its
|
|
257
|
+
extension.
|
|
258
|
+
"""
|
|
259
|
+
domain = _safe_sorted(structure.domain)
|
|
260
|
+
sentences: List[str] = [
|
|
261
|
+
f"The domain has {_count_word(len(domain), 'individual')}: "
|
|
262
|
+
f"{', '.join(_s(d) for d in domain)}."
|
|
263
|
+
]
|
|
264
|
+
|
|
265
|
+
if structure.constants:
|
|
266
|
+
parts = [f"{name} denotes {_s(structure.constants[name])}"
|
|
267
|
+
for name in sorted(structure.constants)]
|
|
268
|
+
label = "constant" if len(parts) == 1 else "constants"
|
|
269
|
+
sentences.append(f"The {label} {'; '.join(parts)}.")
|
|
270
|
+
|
|
271
|
+
for key in sorted(structure.predicates):
|
|
272
|
+
name, arity = key
|
|
273
|
+
extension = structure.predicates[key]
|
|
274
|
+
if arity == 0:
|
|
275
|
+
truth = "true" if bool(extension) else "false"
|
|
276
|
+
sentences.append(f"The nullary predicate {name} is {truth}.")
|
|
277
|
+
continue
|
|
278
|
+
tuples = _safe_sorted(extension)
|
|
279
|
+
if not tuples:
|
|
280
|
+
sentences.append(f"The predicate {name}/{arity} holds for no tuples.")
|
|
281
|
+
else:
|
|
282
|
+
sentences.append(
|
|
283
|
+
f"The predicate {name}/{arity} holds for: {_format_tuple_list(tuples)}."
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
for key in sorted(structure.functions):
|
|
287
|
+
name, arity = key
|
|
288
|
+
mapping = _resolve_function_mapping(structure.functions[key], domain, arity)
|
|
289
|
+
if mapping is None:
|
|
290
|
+
sentences.append(
|
|
291
|
+
f"The function {name}/{arity} is defined procedurally; "
|
|
292
|
+
"its extension was not enumerated."
|
|
293
|
+
)
|
|
294
|
+
elif not mapping:
|
|
295
|
+
sentences.append(f"The function {name}/{arity} has no defined values.")
|
|
296
|
+
else:
|
|
297
|
+
items = _safe_sorted(mapping.items())
|
|
298
|
+
pairs = ", ".join(f"{_format_tuple(k)} → {_s(v)}" for k, v in items)
|
|
299
|
+
sentences.append(f"The function {name}/{arity} maps {pairs}.")
|
|
300
|
+
|
|
301
|
+
return " ".join(sentences[:max_sentences])
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# ---------------------------------------------------------------------------
|
|
305
|
+
# Z3-assignment branch (bare dict, or the "assignment" payload of a z3_model
|
|
306
|
+
# witness dict)
|
|
307
|
+
# ---------------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
def _explain_z3_assignment(assignment: Dict[str, Any], max_sentences: int) -> str:
|
|
310
|
+
"""Explain a Z3-style ``{name: value}`` assignment."""
|
|
311
|
+
if not assignment:
|
|
312
|
+
return "Z3 produced a model, but it recorded no variable assignments."
|
|
313
|
+
items = sorted(assignment.items(), key=lambda kv: str(kv[0]))
|
|
314
|
+
assigned_str = ", ".join(f"{_s(k)} := {_s(v)}" for k, v in items)
|
|
315
|
+
sentences = [
|
|
316
|
+
f"Z3 found a model with {_count_word(len(items), 'assignment')}.",
|
|
317
|
+
f"Under the assignment {assigned_str}, the two sides differ.",
|
|
318
|
+
]
|
|
319
|
+
return " ".join(sentences[:max_sentences])
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# ---------------------------------------------------------------------------
|
|
323
|
+
# Repr-only witness branch (Verdict-layer dicts carrying no structured payload)
|
|
324
|
+
# ---------------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
def _explain_repr_witness(kind: Optional[str], repr_value: Any,
|
|
327
|
+
max_sentences: int) -> str:
|
|
328
|
+
"""Frame an opaque ``repr()`` string as an explicitly-unstructured witness.
|
|
329
|
+
|
|
330
|
+
Deliberately does NOT attempt to parse or reformat ``repr_value`` — it is
|
|
331
|
+
presented verbatim inside an explanatory sentence so the reader can see
|
|
332
|
+
exactly, and only, what was actually recovered.
|
|
333
|
+
"""
|
|
334
|
+
label = kind if kind else "unlabelled"
|
|
335
|
+
sentences = [
|
|
336
|
+
f'This countermodel was reported as a "{label}" witness carrying only '
|
|
337
|
+
"its Python repr, not a structured payload.",
|
|
338
|
+
f"The repr reads: {_s(repr_value)}",
|
|
339
|
+
]
|
|
340
|
+
return " ".join(sentences[:max_sentences])
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# ---------------------------------------------------------------------------
|
|
344
|
+
# Public entry point
|
|
345
|
+
# ---------------------------------------------------------------------------
|
|
346
|
+
|
|
347
|
+
def explain_countermodel(model: Any, formula: Optional[Node] = None, *,
|
|
348
|
+
max_sentences: int = 6) -> str:
|
|
349
|
+
"""Render a countermodel as 2-6 short, deterministic English sentences.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
model: the countermodel to explain. One of:
|
|
353
|
+
|
|
354
|
+
- a :class:`~unicode_logic_kit.semantics.kripke.KripkeModel`;
|
|
355
|
+
- a :class:`~unicode_logic_kit.semantics.tarski.Structure`;
|
|
356
|
+
- a bare Z3-style assignment ``dict`` (``{name: value}``, no
|
|
357
|
+
``"kind"`` key — the shape of ``Verdict.countermodel["assignment"]``);
|
|
358
|
+
- a Verdict-layer witness ``dict`` carrying a ``"kind"`` key:
|
|
359
|
+
``{"kind": "z3_model", "assignment": {...}}`` (routed to the same
|
|
360
|
+
handling as the bare-dict case), or ``{"kind": ..., "repr": "..."}``
|
|
361
|
+
(any other kind — ``"kripke"``, ``"finite_structure"``,
|
|
362
|
+
``"nitpick"``, or a future one — framed as an unparsed witness).
|
|
363
|
+
formula: only consulted for a ``KripkeModel``, and only to decide
|
|
364
|
+
whether to append the fixed sentence "At world 0 the formula
|
|
365
|
+
fails." — see the module docstring's honesty note. Ignored for
|
|
366
|
+
every other ``model`` shape.
|
|
367
|
+
max_sentences: upper bound on the number of sentences returned (at
|
|
368
|
+
least 1 is enforced). Content beyond the cap is dropped, not
|
|
369
|
+
summarised — see each ``_explain_*`` helper for the priority order
|
|
370
|
+
that decides what survives.
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
A plain-text string: sentences separated by single spaces, no
|
|
374
|
+
Markdown, no embedded newlines. Calling this twice on the same
|
|
375
|
+
arguments always returns the identical string (see the module
|
|
376
|
+
docstring's determinism note).
|
|
377
|
+
|
|
378
|
+
Raises:
|
|
379
|
+
ValueError: ``model`` is a witness ``dict`` whose ``"kind"`` is
|
|
380
|
+
recognised as ``"z3_model"`` but has no usable ``"assignment"``
|
|
381
|
+
sub-dict, and it also carries no ``"repr"`` fallback — i.e. there
|
|
382
|
+
is nothing in it to explain.
|
|
383
|
+
TypeError: ``model`` is not one of the four accepted shapes.
|
|
384
|
+
"""
|
|
385
|
+
max_sentences = max(1, max_sentences)
|
|
386
|
+
|
|
387
|
+
if isinstance(model, KripkeModel):
|
|
388
|
+
return _explain_kripke(model, formula, max_sentences)
|
|
389
|
+
|
|
390
|
+
if isinstance(model, Structure):
|
|
391
|
+
return _explain_structure(model, max_sentences)
|
|
392
|
+
|
|
393
|
+
if isinstance(model, dict):
|
|
394
|
+
kind = model.get("kind")
|
|
395
|
+
if kind is None:
|
|
396
|
+
return _explain_z3_assignment(model, max_sentences)
|
|
397
|
+
if kind == "z3_model":
|
|
398
|
+
assignment = model.get("assignment")
|
|
399
|
+
if isinstance(assignment, dict):
|
|
400
|
+
return _explain_z3_assignment(assignment, max_sentences)
|
|
401
|
+
if "repr" in model:
|
|
402
|
+
return _explain_repr_witness(kind, model["repr"], max_sentences)
|
|
403
|
+
raise ValueError(
|
|
404
|
+
f"explain_countermodel: witness dict has kind={kind!r} but neither "
|
|
405
|
+
"a usable 'assignment' dict (for kind='z3_model') nor a 'repr' "
|
|
406
|
+
f"fallback to explain (keys present: {sorted(model)})."
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
raise TypeError(
|
|
410
|
+
f"explain_countermodel: unsupported model type {type(model).__name__} — "
|
|
411
|
+
"expected a KripkeModel, a Structure, a Z3 assignment dict, or a "
|
|
412
|
+
"Verdict-layer witness dict with a 'kind' key."
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# ---------------------------------------------------------------------------
|
|
417
|
+
# explain_proof — the validity-side counterpart of explain_countermodel
|
|
418
|
+
# ---------------------------------------------------------------------------
|
|
419
|
+
#
|
|
420
|
+
# Five proof shapes actually reach ``Verdict.proof`` today (verified by
|
|
421
|
+
# grepping every ``proof=`` assignment in ``unicode_logic_kit/atp/``):
|
|
422
|
+
#
|
|
423
|
+
# * :class:`~unicode_logic_kit.atp.tableau.TableauProof` — ``TableauBackend``.
|
|
424
|
+
# * :class:`~unicode_logic_kit.atp.tstp.TstpDerivation` — ``VampireBackend`` and
|
|
425
|
+
# ``EProverBackend`` (same shape, one renderer covers both).
|
|
426
|
+
# * :class:`~unicode_logic_kit.atp.twee_entailment.TweeProof` — ``TweeBackend``.
|
|
427
|
+
# * ``{"kind": "z3_unsat_core", "core": [...]}`` — ``Z3Backend``.
|
|
428
|
+
# * ``{"kind": "cvc5_alethe", "text": ..., "unsat_core": [...]}`` —
|
|
429
|
+
# :class:`~unicode_logic_kit.atp.cvc5_backend.Cvc5Backend`.
|
|
430
|
+
#
|
|
431
|
+
# The first three also round-trip through ``.to_dict()`` (the shape
|
|
432
|
+
# ``Verdict.proof`` actually carries once a verdict has crossed the
|
|
433
|
+
# process-pool boundary — see ``atp.portfolio._verdict_from_dict``); the last
|
|
434
|
+
# two are ALWAYS plain dicts, since no richer dataclass wraps them. Every
|
|
435
|
+
# renderer below therefore accepts both the typed instance and its dict via
|
|
436
|
+
# the ``_field``/``_node_str`` helpers, and the five key sets are disjoint (a
|
|
437
|
+
# ``"kind"`` key picks the two Z3/cvc5 shapes; among the rest, only
|
|
438
|
+
# ``TableauProof`` carries ``"root_formulas"``/``"closures"``, only
|
|
439
|
+
# ``TweeProof`` carries ``"axioms"``/``"lemmas"``/``"goal"``, and a bare
|
|
440
|
+
# ``{"steps"}`` is ``TstpDerivation``) — see :func:`explain_proof`'s dispatch.
|
|
441
|
+
#
|
|
442
|
+
# Fitch ``Proof``/``Line``/``Justification`` chains (``atp.fitch_search``) are
|
|
443
|
+
# deliberately NOT covered: no ``ProverBackend`` currently attaches one to
|
|
444
|
+
# ``Verdict.proof``, so there is no live caller through the eval/atp protocol
|
|
445
|
+
# layer to explain today — a natural follow-up once/if one is registered.
|
|
446
|
+
|
|
447
|
+
def _field(obj: Any, key: str) -> Any:
|
|
448
|
+
"""Read ``key`` from ``obj``, whether it is a proof dataclass instance or
|
|
449
|
+
the matching ``to_dict()``-shaped plain dict — both share field names, so
|
|
450
|
+
this is the one place that bridges "typed object" and "Verdict.proof
|
|
451
|
+
dict" for every renderer below."""
|
|
452
|
+
return obj[key] if isinstance(obj, dict) else getattr(obj, key)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _node_str(value: Any) -> str:
|
|
456
|
+
"""Render a formula field as Unicode text — ``value`` is a :class:`Node`
|
|
457
|
+
when the caller passed typed proof objects, or a ``Node.to_dict()`` dict
|
|
458
|
+
when it passed the plain ``Verdict.proof`` dict."""
|
|
459
|
+
if isinstance(value, Node):
|
|
460
|
+
return value.to_unicode_str()
|
|
461
|
+
if isinstance(value, dict):
|
|
462
|
+
return Node.from_dict(value).to_unicode_str()
|
|
463
|
+
raise TypeError(
|
|
464
|
+
f"explain_proof: expected a Node or a Node.to_dict() dict, got "
|
|
465
|
+
f"{type(value).__name__}"
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
# ---------------------------------------------------------------------------
|
|
470
|
+
# TableauProof branch
|
|
471
|
+
# ---------------------------------------------------------------------------
|
|
472
|
+
|
|
473
|
+
def _explain_tableau(proof: Any, max_sentences: int) -> str:
|
|
474
|
+
"""Explain a tableau refutation.
|
|
475
|
+
|
|
476
|
+
Sentence priority (highest first):
|
|
477
|
+
|
|
478
|
+
1. Step count.
|
|
479
|
+
2. A sorted (by rule name) histogram of rule applications — omitted if
|
|
480
|
+
there are no steps (a tableau that closes at the root needs none).
|
|
481
|
+
3. Closed-branch count (``len(closures)``).
|
|
482
|
+
4. One sentence per closure, sorted by ``leaf_id``, naming the two
|
|
483
|
+
closing literals — or, for a self-closing branch (``literal`` is ⊥,
|
|
484
|
+
``complement`` is ``None`` per :class:`TableauClosure`'s own
|
|
485
|
+
docstring), stating that directly rather than inventing a complement.
|
|
486
|
+
"""
|
|
487
|
+
steps = list(_field(proof, "steps"))
|
|
488
|
+
closures = list(_field(proof, "closures"))
|
|
489
|
+
|
|
490
|
+
sentences: List[str] = [
|
|
491
|
+
f"The tableau proof has {_count_word(len(steps), 'step')}."
|
|
492
|
+
]
|
|
493
|
+
|
|
494
|
+
histogram: Dict[str, int] = {}
|
|
495
|
+
for step in steps:
|
|
496
|
+
rule = _field(step, "rule")
|
|
497
|
+
histogram[rule] = histogram.get(rule, 0) + 1
|
|
498
|
+
if histogram:
|
|
499
|
+
parts = [f"{rule} ({count})" for rule, count in sorted(histogram.items())]
|
|
500
|
+
sentences.append(f"Rule usage: {', '.join(parts)}.")
|
|
501
|
+
|
|
502
|
+
branch_word = "closed branch" if len(closures) == 1 else "closed branches"
|
|
503
|
+
sentences.append(f"The proof has {len(closures)} {branch_word}.")
|
|
504
|
+
|
|
505
|
+
for closure in sorted(closures, key=lambda c: _field(c, "leaf_id")):
|
|
506
|
+
leaf_id = _field(closure, "leaf_id")
|
|
507
|
+
literal = _node_str(_field(closure, "literal"))
|
|
508
|
+
complement = _field(closure, "complement")
|
|
509
|
+
if complement is None:
|
|
510
|
+
sentences.append(f"Branch closing at node {leaf_id} closes directly on {literal}.")
|
|
511
|
+
else:
|
|
512
|
+
sentences.append(
|
|
513
|
+
f"Branch closing at node {leaf_id} closes {literal} against "
|
|
514
|
+
f"{_node_str(complement)}."
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
return " ".join(sentences[:max_sentences])
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
# ---------------------------------------------------------------------------
|
|
521
|
+
# TstpDerivation branch (Vampire and E — same shape, same renderer)
|
|
522
|
+
# ---------------------------------------------------------------------------
|
|
523
|
+
|
|
524
|
+
def _explain_tstp(proof: Any, max_sentences: int) -> str:
|
|
525
|
+
"""Explain a TSTP derivation DAG.
|
|
526
|
+
|
|
527
|
+
Sentence priority (highest first):
|
|
528
|
+
|
|
529
|
+
1. Step count.
|
|
530
|
+
2. The sorted set of roles present (``axiom``, ``negated_conjecture``, …).
|
|
531
|
+
3. The final step's rule (or, if it names none, that it is an
|
|
532
|
+
unjustified leaf) together with its ancestor trace: a backward
|
|
533
|
+
breadth-first walk of the parent DAG rooted at the final step, listed
|
|
534
|
+
layer by layer — so a multi-level derivation names every ancestor
|
|
535
|
+
reached from the last step, not just one arbitrarily-chosen path.
|
|
536
|
+
A cited parent name absent from this derivation's own steps (e.g. an
|
|
537
|
+
external ``file(...)`` source never itself recorded) is left
|
|
538
|
+
unexpanded rather than guessed at.
|
|
539
|
+
"""
|
|
540
|
+
steps = list(_field(proof, "steps"))
|
|
541
|
+
if not steps:
|
|
542
|
+
raise ValueError("explain_proof: TstpDerivation has no steps to explain.")
|
|
543
|
+
|
|
544
|
+
sentences: List[str] = [f"The derivation has {_count_word(len(steps), 'step')}."]
|
|
545
|
+
|
|
546
|
+
roles = sorted({_field(s, "role") for s in steps})
|
|
547
|
+
sentences.append(f"Roles present: {', '.join(roles)}.")
|
|
548
|
+
|
|
549
|
+
by_name = {_field(s, "name"): s for s in steps}
|
|
550
|
+
final = steps[-1]
|
|
551
|
+
final_name = _field(final, "name")
|
|
552
|
+
final_rule = _field(final, "rule")
|
|
553
|
+
|
|
554
|
+
if final_rule is None:
|
|
555
|
+
sentences.append(
|
|
556
|
+
f"The final step {final_name} is a leaf with no inference rule recorded."
|
|
557
|
+
)
|
|
558
|
+
else:
|
|
559
|
+
seen = {final_name}
|
|
560
|
+
order: List[str] = []
|
|
561
|
+
frontier = list(_field(final, "parents"))
|
|
562
|
+
while frontier:
|
|
563
|
+
next_frontier: List[str] = []
|
|
564
|
+
for name in frontier:
|
|
565
|
+
if name in seen:
|
|
566
|
+
continue
|
|
567
|
+
seen.add(name)
|
|
568
|
+
order.append(name)
|
|
569
|
+
parent_step = by_name.get(name)
|
|
570
|
+
if parent_step is not None:
|
|
571
|
+
next_frontier.extend(_field(parent_step, "parents"))
|
|
572
|
+
frontier = next_frontier
|
|
573
|
+
if order:
|
|
574
|
+
sentences.append(
|
|
575
|
+
f"The final step {final_name} applies {final_rule}, tracing "
|
|
576
|
+
f"back through {', '.join(order)}."
|
|
577
|
+
)
|
|
578
|
+
else:
|
|
579
|
+
sentences.append(
|
|
580
|
+
f"The final step {final_name} applies {final_rule} with no "
|
|
581
|
+
"recorded parents."
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
return " ".join(sentences[:max_sentences])
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
# ---------------------------------------------------------------------------
|
|
588
|
+
# TweeProof branch
|
|
589
|
+
# ---------------------------------------------------------------------------
|
|
590
|
+
|
|
591
|
+
def _format_twee_citation(citation: Any) -> str:
|
|
592
|
+
"""Render one ``{ by axiom N (name) [R->L] }`` / ``{ by lemma N [R->L] }``
|
|
593
|
+
citation as ``"axiom N (name)"`` / ``"lemma N"``, with an " R->L" suffix
|
|
594
|
+
when the citation applies its equation right-to-left."""
|
|
595
|
+
kind = _field(citation, "kind")
|
|
596
|
+
number = _field(citation, "number")
|
|
597
|
+
name = _field(citation, "name")
|
|
598
|
+
text = f"{kind} {number}" + (f" ({name})" if name else "")
|
|
599
|
+
if _field(citation, "reversed"):
|
|
600
|
+
text += " R->L"
|
|
601
|
+
return text
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def _explain_twee(proof: Any, max_sentences: int) -> str:
|
|
605
|
+
"""Explain a Twee equational proof.
|
|
606
|
+
|
|
607
|
+
Sentence priority (highest first):
|
|
608
|
+
|
|
609
|
+
1. Axiom and lemma counts.
|
|
610
|
+
2. The goal's own equation, by name.
|
|
611
|
+
3. The goal's rewrite chain, term by term.
|
|
612
|
+
4. The chain's citations, in step order — or an explicit "no citations"
|
|
613
|
+
sentence for the (degenerate, single-term) chain that has none.
|
|
614
|
+
"""
|
|
615
|
+
axioms = list(_field(proof, "axioms"))
|
|
616
|
+
lemmas = list(_field(proof, "lemmas"))
|
|
617
|
+
goal = _field(proof, "goal")
|
|
618
|
+
|
|
619
|
+
sentences: List[str] = [
|
|
620
|
+
f"The proof uses {_count_word(len(axioms), 'axiom')} and "
|
|
621
|
+
f"{_count_word(len(lemmas), 'lemma')}."
|
|
622
|
+
]
|
|
623
|
+
|
|
624
|
+
goal_name = _field(goal, "name")
|
|
625
|
+
equation = _field(goal, "equation")
|
|
626
|
+
sentences.append(
|
|
627
|
+
f"The goal ({goal_name}) states "
|
|
628
|
+
f"{_node_str(_field(equation, 'lhs'))} = {_node_str(_field(equation, 'rhs'))}."
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
chain = _field(goal, "chain")
|
|
632
|
+
terms = [_node_str(t) for t in _field(chain, "terms")]
|
|
633
|
+
sentences.append(f"It rewrites {' → '.join(terms)}.")
|
|
634
|
+
|
|
635
|
+
citations = list(_field(chain, "citations"))
|
|
636
|
+
if citations:
|
|
637
|
+
cite_strs = [_format_twee_citation(c) for c in citations]
|
|
638
|
+
sentences.append(f"Citations: {', '.join(cite_strs)}.")
|
|
639
|
+
else:
|
|
640
|
+
sentences.append("No citations are recorded for this chain.")
|
|
641
|
+
|
|
642
|
+
return " ".join(sentences[:max_sentences])
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
# ---------------------------------------------------------------------------
|
|
646
|
+
# Z3Backend's unsat-core proof dict
|
|
647
|
+
# ---------------------------------------------------------------------------
|
|
648
|
+
|
|
649
|
+
def _explain_z3_unsat_core(proof: Dict[str, Any], max_sentences: int) -> str:
|
|
650
|
+
"""Explain ``{"kind": "z3_unsat_core", "core": [...]}``.
|
|
651
|
+
|
|
652
|
+
``core`` is a sound but not necessarily minimal unsat core (see
|
|
653
|
+
``Z3Backend.decide``'s own comment) — reported as exactly that, every
|
|
654
|
+
tracked name listed in the order Z3/the backend already sorted them.
|
|
655
|
+
"""
|
|
656
|
+
core = list(proof.get("core") or [])
|
|
657
|
+
if not core:
|
|
658
|
+
return "Z3 refutes the goal via an empty unsat core."
|
|
659
|
+
sentences = [
|
|
660
|
+
f"Z3 refutes the goal via an unsat core of "
|
|
661
|
+
f"{_count_word(len(core), 'tracked term')}: {', '.join(_s(c) for c in core)}."
|
|
662
|
+
]
|
|
663
|
+
return " ".join(sentences[:max_sentences])
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
# ---------------------------------------------------------------------------
|
|
667
|
+
# CVC5Backend's Alethe proof dict
|
|
668
|
+
# ---------------------------------------------------------------------------
|
|
669
|
+
|
|
670
|
+
def _explain_cvc5_alethe(proof: Dict[str, Any], max_sentences: int) -> str:
|
|
671
|
+
"""Explain ``{"kind": "cvc5_alethe", "text": ..., "unsat_core": [...]}``.
|
|
672
|
+
|
|
673
|
+
``text`` (the Alethe proof, best-effort — see ``Cvc5Backend.decide``'s own
|
|
674
|
+
comment) is summarised by its non-blank line count rather than quoted in
|
|
675
|
+
full; ``unsat_core`` is the same "sound, not necessarily minimal" shape as
|
|
676
|
+
Z3's. Either can be empty/``None`` without this being an error — both are
|
|
677
|
+
reported honestly rather than guessed at.
|
|
678
|
+
"""
|
|
679
|
+
text = proof.get("text")
|
|
680
|
+
core = list(proof.get("unsat_core") or [])
|
|
681
|
+
|
|
682
|
+
sentences: List[str] = []
|
|
683
|
+
if text:
|
|
684
|
+
line_count = len([ln for ln in text.splitlines() if ln.strip()])
|
|
685
|
+
sentences.append(
|
|
686
|
+
f"cvc5 refutes the goal with an Alethe proof of "
|
|
687
|
+
f"{_count_word(line_count, 'line')}."
|
|
688
|
+
)
|
|
689
|
+
else:
|
|
690
|
+
sentences.append("cvc5 refutes the goal; no Alethe proof text was recorded.")
|
|
691
|
+
|
|
692
|
+
if core:
|
|
693
|
+
sentences.append(
|
|
694
|
+
f"Its unsat core cites {_count_word(len(core), 'term')}: "
|
|
695
|
+
f"{', '.join(_s(c) for c in core)}."
|
|
696
|
+
)
|
|
697
|
+
else:
|
|
698
|
+
sentences.append("Its unsat core is empty.")
|
|
699
|
+
|
|
700
|
+
return " ".join(sentences[:max_sentences])
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
# ---------------------------------------------------------------------------
|
|
704
|
+
# Public entry point
|
|
705
|
+
# ---------------------------------------------------------------------------
|
|
706
|
+
|
|
707
|
+
def explain_proof(proof: Any, *, max_sentences: int = 6) -> str:
|
|
708
|
+
"""Render a proof as short, deterministic English sentences.
|
|
709
|
+
|
|
710
|
+
The validity-side counterpart of :func:`explain_countermodel`: where that
|
|
711
|
+
function explains why a formula is *not* valid (a witnessing structure),
|
|
712
|
+
this one explains why it *is* (a proof search's own record of how it
|
|
713
|
+
closed) — see the module docstring for the honesty/determinism
|
|
714
|
+
discipline both share.
|
|
715
|
+
|
|
716
|
+
Args:
|
|
717
|
+
proof: the proof to explain. One of:
|
|
718
|
+
|
|
719
|
+
- a :class:`~unicode_logic_kit.atp.tableau.TableauProof` (or its
|
|
720
|
+
``.to_dict()``) — from ``TableauBackend``;
|
|
721
|
+
- a :class:`~unicode_logic_kit.atp.tstp.TstpDerivation` (or its
|
|
722
|
+
``.to_dict()``) — from ``VampireBackend``/``EProverBackend``;
|
|
723
|
+
- a :class:`~unicode_logic_kit.atp.twee_entailment.TweeProof` (or
|
|
724
|
+
its ``.to_dict()``) — from ``TweeBackend``;
|
|
725
|
+
- ``{"kind": "z3_unsat_core", "core": [...]}`` — from
|
|
726
|
+
``Z3Backend``;
|
|
727
|
+
- ``{"kind": "cvc5_alethe", "text": ..., "unsat_core": [...]}`` —
|
|
728
|
+
from ``Cvc5Backend``.
|
|
729
|
+
|
|
730
|
+
See the module-level comment above this section for how the five
|
|
731
|
+
shapes are told apart unambiguously.
|
|
732
|
+
max_sentences: upper bound on the number of sentences returned (at
|
|
733
|
+
least 1 is enforced). Content beyond the cap is dropped, not
|
|
734
|
+
summarised — see each ``_explain_*`` helper for the priority
|
|
735
|
+
order that decides what survives.
|
|
736
|
+
|
|
737
|
+
Returns:
|
|
738
|
+
A plain-text string: sentences separated by single spaces, no
|
|
739
|
+
Markdown, no embedded newlines (every value rendered into a sentence
|
|
740
|
+
is either a :meth:`Node.to_unicode_str` result or a Python string
|
|
741
|
+
already free of embedded newlines by construction). Calling this
|
|
742
|
+
twice on the same argument always returns the identical string.
|
|
743
|
+
|
|
744
|
+
Raises:
|
|
745
|
+
ValueError: ``proof`` is a :class:`TstpDerivation` (or its dict) with
|
|
746
|
+
no steps, or a dict carrying a ``"kind"`` this function does not
|
|
747
|
+
recognise, or a dict whose keys match none of the five accepted
|
|
748
|
+
shapes.
|
|
749
|
+
TypeError: ``proof`` is not one of the five accepted shapes at all.
|
|
750
|
+
"""
|
|
751
|
+
max_sentences = max(1, max_sentences)
|
|
752
|
+
|
|
753
|
+
if isinstance(proof, TableauProof):
|
|
754
|
+
return _explain_tableau(proof, max_sentences)
|
|
755
|
+
if isinstance(proof, TstpDerivation):
|
|
756
|
+
return _explain_tstp(proof, max_sentences)
|
|
757
|
+
if isinstance(proof, TweeProof):
|
|
758
|
+
return _explain_twee(proof, max_sentences)
|
|
759
|
+
|
|
760
|
+
if isinstance(proof, dict):
|
|
761
|
+
keys = set(proof)
|
|
762
|
+
if "kind" in proof:
|
|
763
|
+
kind = proof["kind"]
|
|
764
|
+
if kind == "z3_unsat_core":
|
|
765
|
+
return _explain_z3_unsat_core(proof, max_sentences)
|
|
766
|
+
if kind == "cvc5_alethe":
|
|
767
|
+
return _explain_cvc5_alethe(proof, max_sentences)
|
|
768
|
+
raise ValueError(
|
|
769
|
+
f"explain_proof: unrecognised proof dict kind={kind!r} — "
|
|
770
|
+
"expected 'z3_unsat_core' or 'cvc5_alethe' "
|
|
771
|
+
f"(keys present: {sorted(keys)})."
|
|
772
|
+
)
|
|
773
|
+
if {"root_formulas", "steps", "closures"} <= keys:
|
|
774
|
+
return _explain_tableau(proof, max_sentences)
|
|
775
|
+
if keys == {"steps"}:
|
|
776
|
+
return _explain_tstp(proof, max_sentences)
|
|
777
|
+
if {"axioms", "lemmas", "goal"} <= keys:
|
|
778
|
+
return _explain_twee(proof, max_sentences)
|
|
779
|
+
raise ValueError(
|
|
780
|
+
"explain_proof: unrecognised proof dict shape — expected a "
|
|
781
|
+
"TableauProof ({'root_formulas','steps','closures'}), a "
|
|
782
|
+
"TstpDerivation ({'steps'}), a TweeProof "
|
|
783
|
+
"({'axioms','lemmas','goal'}), or a 'kind'-tagged Verdict-layer "
|
|
784
|
+
f"dict (keys present: {sorted(keys)})."
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
raise TypeError(
|
|
788
|
+
f"explain_proof: unsupported proof type {type(proof).__name__} — "
|
|
789
|
+
"expected a TableauProof, TstpDerivation, TweeProof, or one of the "
|
|
790
|
+
"'kind'-tagged Verdict-layer proof dicts (z3_unsat_core, cvc5_alethe)."
|
|
791
|
+
)
|