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,389 @@
|
|
|
1
|
+
"""Reading a learner's answer back as a kit formula.
|
|
2
|
+
|
|
3
|
+
:func:`~unicode_logic_kit.fol.parse_prolog_clause` already turns a Prolog clause
|
|
4
|
+
into an AST. One step is missing before the result can be model-checked: the
|
|
5
|
+
**membership atom has to go**. ``atom_in(A, B)`` says "B is one of the things
|
|
6
|
+
in example A" — it anchors the clause to the example set and says nothing
|
|
7
|
+
whatever about the structure being checked. Left in, it is a predicate no
|
|
8
|
+
structure interprets, and evaluation raises
|
|
9
|
+
:class:`~unicode_logic_kit.semantics.model_eval.UninterpretedSymbol` instead of
|
|
10
|
+
deciding anything.
|
|
11
|
+
|
|
12
|
+
Dropping it is also where the encoding's soundness gets checked a second time,
|
|
13
|
+
from the other side. Two shapes are refused rather than translated:
|
|
14
|
+
|
|
15
|
+
* **A membership atom whose first argument is not the head's variable.** That
|
|
16
|
+
is the learner naming a SECOND example — the clause is then a statement
|
|
17
|
+
about a pair of examples, and there is no formula about one structure that
|
|
18
|
+
means the same thing.
|
|
19
|
+
* **The head variable surviving the drop.** Same problem, found by its
|
|
20
|
+
symptom rather than its shape.
|
|
21
|
+
* **A body goal the membership atoms never reached.** In
|
|
22
|
+
``t(A) :- n(C), atom_in(A, B), o(B).`` the goal ``n(C)`` is not connected to
|
|
23
|
+
``B`` by anything, so in Prolog it ranges over the WHOLE fact base: it
|
|
24
|
+
succeeds if *any* example has a nitrogen, which makes the clause true of
|
|
25
|
+
every example at once. Reading it as ``∃c N(c)`` over one structure would
|
|
26
|
+
quietly turn a global claim into a local one — and a learner's clause that
|
|
27
|
+
covers a negative example would come back scoring perfectly. Linkage is
|
|
28
|
+
therefore required, and it propagates only through POSITIVE goals, because
|
|
29
|
+
``\\+`` binds nothing in SLDNF.
|
|
30
|
+
|
|
31
|
+
The first two are the second trap from the package docstring, caught after the
|
|
32
|
+
fact; the third is the third trap in disguise (an unlinked goal is effectively
|
|
33
|
+
0-ary — it holds globally). A task built by
|
|
34
|
+
:class:`~unicode_logic_kit.ilp.IlpTask` cannot produce the first two, and a
|
|
35
|
+
learner searching a task built by it will not usually produce the third; these
|
|
36
|
+
checks exist for clauses that came from somewhere else, and for the learner
|
|
37
|
+
that surprises you.
|
|
38
|
+
|
|
39
|
+
What is left is closed existentially: every variable of the remaining body,
|
|
40
|
+
including the ones the membership atom introduced. That is the right reading —
|
|
41
|
+
``atom_in(A, B)`` restricted ``B`` to the individuals of example ``A``, and
|
|
42
|
+
once the example is the structure being checked, "some individual of the
|
|
43
|
+
example" is exactly ``∃b`` over its domain. Top-level ``∃`` is also the right
|
|
44
|
+
scope for a variable under ``\\+``: linkage guarantees such a variable also
|
|
45
|
+
occurs in a positive goal, which is what binds it before the negation is
|
|
46
|
+
tested, so ``∃c (P(c) ∧ ¬Q(c))`` — and not the much weaker ``∃c ¬Q(c)`` — is
|
|
47
|
+
what comes back.
|
|
48
|
+
|
|
49
|
+
**Predicate names come back exactly as they went out** — if you say which
|
|
50
|
+
ones. The importer capitalises (``bSINGLE`` in, ``BSINGLE`` back), because
|
|
51
|
+
that is the kit's spelling and the right default for a clause from anywhere.
|
|
52
|
+
But a structure's symbols are whatever the structure calls them, so a formula
|
|
53
|
+
in kit spelling cannot be evaluated against one. Pass ``predicates=`` — or
|
|
54
|
+
call :meth:`~unicode_logic_kit.ilp.IlpTask.read_clause`, which passes the task's
|
|
55
|
+
own vocabulary — and the emitted spelling is restored, making the round trip
|
|
56
|
+
name-exact for exactly the symbols the task offered.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
from typing import Dict, Iterable, List, Optional, Sequence, Tuple, Union
|
|
60
|
+
|
|
61
|
+
from ..fol.nodes import (
|
|
62
|
+
And, Atom, Implies, Node, Quantifier, Variable, free_variables,
|
|
63
|
+
)
|
|
64
|
+
from ..fol.prolog_input import parse_prolog_clause
|
|
65
|
+
from .task import IlpEncodingError
|
|
66
|
+
|
|
67
|
+
__all__ = ["clause_to_formula", "hypothesis_to_formulas"]
|
|
68
|
+
|
|
69
|
+
_FORALL = ("∀", "forall")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _cap(name: str) -> str:
|
|
73
|
+
"""The kit's spelling of a Prolog functor — first character up.
|
|
74
|
+
|
|
75
|
+
Mirrors :func:`unicode_logic_kit.fol.prolog_input._cap` rather than
|
|
76
|
+
importing it, for the reason that module gives for not importing the TPTP
|
|
77
|
+
importer's: the two conventions are meant to be visibly the same rule, and
|
|
78
|
+
a divergence should surface as a failing round-trip test.
|
|
79
|
+
"""
|
|
80
|
+
return name[:1].upper() + name[1:] if name else name
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _conjuncts(node: Node) -> List[Node]:
|
|
84
|
+
"""Flatten a top-level ``∧`` tree, left to right."""
|
|
85
|
+
if isinstance(node, And):
|
|
86
|
+
return _conjuncts(node.left) + _conjuncts(node.right)
|
|
87
|
+
return [node]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _conjoin(parts: List[Node]) -> Node:
|
|
91
|
+
result = parts[0]
|
|
92
|
+
for part in parts[1:]:
|
|
93
|
+
result = And(result, part)
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
PredicateSpec = Union[str, Tuple[str, int]]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _rename_map(predicates: Optional[Iterable[PredicateSpec]]
|
|
101
|
+
) -> Dict[Tuple[str, Optional[int]], str]:
|
|
102
|
+
"""``{(capitalised functor, arity): the structure's own spelling}``.
|
|
103
|
+
|
|
104
|
+
Accepts bare names or ``(name, arity)`` pairs, so
|
|
105
|
+
:attr:`~unicode_logic_kit.ilp.IlpTask.body_predicates` can be handed over
|
|
106
|
+
unchanged; a bare name is stored under arity ``None`` and matches any
|
|
107
|
+
arity. Keying by arity matters: ``P/1`` and ``p/2`` emit as the DISTINCT
|
|
108
|
+
Prolog predicates ``p/1`` and ``p/2``, so a clause can say which it meant.
|
|
109
|
+
Only a genuine clash — the same functor at the same arity from two
|
|
110
|
+
different symbols — is refused, and
|
|
111
|
+
:class:`~unicode_logic_kit.ilp.IlpTask` refuses to emit one in the first
|
|
112
|
+
place.
|
|
113
|
+
"""
|
|
114
|
+
mapping: Dict[Tuple[str, Optional[int]], str] = {}
|
|
115
|
+
for entry in predicates or ():
|
|
116
|
+
if isinstance(entry, (tuple, list)):
|
|
117
|
+
name, arity = entry[0], int(entry[1])
|
|
118
|
+
else:
|
|
119
|
+
name, arity = entry, None
|
|
120
|
+
key = (_cap(name), arity)
|
|
121
|
+
if key in mapping and mapping[key] != name:
|
|
122
|
+
raise IlpEncodingError(
|
|
123
|
+
f"clause_to_formula: predicates {mapping[key]!r} and {name!r} "
|
|
124
|
+
f"both arrive as {key[0]!r} at the same arity — the importer "
|
|
125
|
+
"cannot tell them apart, so which one a learned clause meant "
|
|
126
|
+
"is undecidable")
|
|
127
|
+
mapping[key] = name
|
|
128
|
+
return mapping
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _rename(node: Node, mapping: Dict[Tuple[str, Optional[int]], str]) -> Node:
|
|
132
|
+
"""Restore the emitted spelling of every predicate the mapping covers."""
|
|
133
|
+
if isinstance(node, Atom):
|
|
134
|
+
for key in ((node.predicate, len(node.args)), (node.predicate, None)):
|
|
135
|
+
if key in mapping:
|
|
136
|
+
node = Atom(mapping[key], node.args)
|
|
137
|
+
break
|
|
138
|
+
return node.map_children(lambda child: _rename(child, mapping))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _close_existentially(formula: Node) -> Node:
|
|
142
|
+
"""Close every free variable, alphabetically outermost-first.
|
|
143
|
+
|
|
144
|
+
Order among quantifiers of the same kind never changes truth, so this is
|
|
145
|
+
purely so the same clause always renders the same way — the same choice
|
|
146
|
+
:func:`unicode_logic_kit.fol.parse_prolog_clause` makes.
|
|
147
|
+
"""
|
|
148
|
+
result = formula
|
|
149
|
+
for variable in sorted(free_variables(formula), key=lambda v: v.name,
|
|
150
|
+
reverse=True):
|
|
151
|
+
result = Quantifier("∃", variable, result)
|
|
152
|
+
return result
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def clause_to_formula(clause: str, *, membership: str = "atom_in",
|
|
156
|
+
predicates: Optional[Iterable[PredicateSpec]] = None,
|
|
157
|
+
negation_as_failure: str = "refuse") -> Node:
|
|
158
|
+
"""One learned clause → the kit formula it states about a single structure.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
clause: a definite clause as a learner prints it, e.g.
|
|
162
|
+
``"amide(A) :- bSINGLE(C,D), bDOUBLE(D,B), n(C), atom_in(A,B)."``
|
|
163
|
+
— every goal reaching the example through the membership atom, as
|
|
164
|
+
a task built by :class:`~unicode_logic_kit.ilp.IlpTask` forces.
|
|
165
|
+
membership: the predicate carrying the example argument — the same
|
|
166
|
+
name :class:`~unicode_logic_kit.ilp.IlpTask` emitted. Matched after
|
|
167
|
+
the importer's capitalisation, so ``"atom_in"`` matches the
|
|
168
|
+
``Atom_in`` the parser produces.
|
|
169
|
+
predicates: the vocabulary the task offered, as names or
|
|
170
|
+
``(name, arity)`` pairs. Each is restored to that exact spelling,
|
|
171
|
+
undoing the importer's capitalisation so the result can be
|
|
172
|
+
evaluated against the structures it came from. Symbols not listed
|
|
173
|
+
keep the kit spelling — and will then fail loudly as
|
|
174
|
+
uninterpreted, which is the correct outcome for a predicate the
|
|
175
|
+
task never offered.
|
|
176
|
+
negation_as_failure: passed through to
|
|
177
|
+
:func:`~unicode_logic_kit.fol.parse_prolog_clause`; ``"classical"``
|
|
178
|
+
asserts the closed world assumption for the learner's program.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
The body, without membership atoms, existentially closed.
|
|
182
|
+
|
|
183
|
+
Raises:
|
|
184
|
+
~unicode_logic_kit.ilp.IlpEncodingError: the clause is a fact (no
|
|
185
|
+
body), its head is not
|
|
186
|
+
``target(Variable)``, a membership atom names a different example
|
|
187
|
+
or is buried inside a disjunction or negation, nothing but
|
|
188
|
+
membership atoms was there to begin with, the example variable
|
|
189
|
+
survives the drop, or a goal is not linked to the example.
|
|
190
|
+
~unicode_logic_kit.fol.prolog_input.PrologParsingError: the text is
|
|
191
|
+
outside the accepted Prolog fragment.
|
|
192
|
+
|
|
193
|
+
Example:
|
|
194
|
+
>>> from unicode_logic_kit.ilp import clause_to_formula
|
|
195
|
+
>>> learned = "amide(A) :- n(C), bDOUBLE(C,B), atom_in(A,B)."
|
|
196
|
+
>>> clause_to_formula(learned).to_unicode_str()
|
|
197
|
+
'∃b ∃c (N(c) ∧ BDOUBLE(c, b))'
|
|
198
|
+
>>> clause_to_formula(learned, predicates=["n", "bDOUBLE"]
|
|
199
|
+
... ).to_unicode_str()
|
|
200
|
+
'∃b ∃c (n(c) ∧ bDOUBLE(c, b))'
|
|
201
|
+
"""
|
|
202
|
+
node = parse_prolog_clause(clause, mode="clause",
|
|
203
|
+
negation_as_failure=negation_as_failure)
|
|
204
|
+
matrix = node
|
|
205
|
+
while isinstance(matrix, Quantifier) and matrix.type in _FORALL:
|
|
206
|
+
matrix = matrix.formula
|
|
207
|
+
|
|
208
|
+
head, body = _split_implication(matrix, clause)
|
|
209
|
+
if not isinstance(head, Atom) or len(head.args) != 1:
|
|
210
|
+
raise IlpEncodingError(
|
|
211
|
+
f"clause_to_formula: the head must be target(Example) with exactly "
|
|
212
|
+
f"one argument, got {head.to_unicode_str()!r}")
|
|
213
|
+
example = head.args[0]
|
|
214
|
+
if not isinstance(example, Variable):
|
|
215
|
+
raise IlpEncodingError(
|
|
216
|
+
"clause_to_formula: the head's argument must be a VARIABLE (the "
|
|
217
|
+
f"example), got {example.to_unicode_str()!r} — a ground head is a "
|
|
218
|
+
"fact about one example, not a rule")
|
|
219
|
+
|
|
220
|
+
wanted = _cap(membership)
|
|
221
|
+
kept: List[Node] = []
|
|
222
|
+
seeds: set = set()
|
|
223
|
+
dropped = 0
|
|
224
|
+
for conjunct in _conjuncts(body):
|
|
225
|
+
if _is_membership(conjunct, wanted):
|
|
226
|
+
_check_membership(conjunct, example, membership)
|
|
227
|
+
seeds |= conjunct.args[1].variables()
|
|
228
|
+
dropped += 1
|
|
229
|
+
continue
|
|
230
|
+
buried = [a for a in conjunct.atoms() if _is_membership(a, wanted)]
|
|
231
|
+
if buried:
|
|
232
|
+
raise IlpEncodingError(
|
|
233
|
+
f"clause_to_formula: a {membership} atom occurs inside "
|
|
234
|
+
f"{conjunct.to_unicode_str()!r} rather than as a top-level "
|
|
235
|
+
"condition. Dropping it there would change what the clause "
|
|
236
|
+
"says, so it is refused instead.")
|
|
237
|
+
kept.append(conjunct)
|
|
238
|
+
|
|
239
|
+
if not kept:
|
|
240
|
+
raise IlpEncodingError(
|
|
241
|
+
f"clause_to_formula: the clause has no content besides its "
|
|
242
|
+
f"{dropped} {membership} atom(s) — it says only that the example "
|
|
243
|
+
"has an individual, which is true of every non-empty structure")
|
|
244
|
+
|
|
245
|
+
remaining = _conjoin(kept)
|
|
246
|
+
# Survival first, linkage second: a surviving example variable is also an
|
|
247
|
+
# unlinked one, and "the example variable is still here" is the more
|
|
248
|
+
# specific diagnosis of the same clause.
|
|
249
|
+
if example in free_variables(remaining):
|
|
250
|
+
raise IlpEncodingError(
|
|
251
|
+
f"clause_to_formula: the example variable "
|
|
252
|
+
f"{example.to_unicode_str()} still occurs after the {membership} "
|
|
253
|
+
"atoms were dropped, so the clause is not a statement about a "
|
|
254
|
+
"single structure. This is the encoding trap the ilp package "
|
|
255
|
+
"exists to prevent: see unicode_logic_kit.ilp's docstring.")
|
|
256
|
+
_check_linked(kept, seeds, membership)
|
|
257
|
+
return _close_existentially(_rename(remaining, _rename_map(predicates)))
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _split_implication(matrix: Node, clause: str):
|
|
261
|
+
"""``Body → Head`` from the universally closed clause, or a clear refusal."""
|
|
262
|
+
if not isinstance(matrix, Implies):
|
|
263
|
+
raise IlpEncodingError(
|
|
264
|
+
f"clause_to_formula: {clause.strip()!r} is a fact, not a rule — "
|
|
265
|
+
"there is no body to turn into a formula")
|
|
266
|
+
return matrix.right, matrix.left
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _check_linked(kept: List[Node], seeds: set, membership: str) -> None:
|
|
270
|
+
"""Every remaining goal must reach the example through the membership atoms.
|
|
271
|
+
|
|
272
|
+
The reachability relation is co-occurrence in a POSITIVE goal, seeded by
|
|
273
|
+
the variables the dropped membership atoms restricted. Negation and
|
|
274
|
+
disjunction contribute no edges — ``\\+ q(B, C)`` binds nothing in SLDNF,
|
|
275
|
+
and which branch of a disjunction binds what is branch-dependent — so a
|
|
276
|
+
variable that occurs only there must already be linked by a positive goal.
|
|
277
|
+
|
|
278
|
+
A goal with no variables at all is refused for the same reason it would
|
|
279
|
+
fail: a ground goal is decided against the whole fact base, not against
|
|
280
|
+
one example.
|
|
281
|
+
"""
|
|
282
|
+
linked = set(seeds)
|
|
283
|
+
positive = [c for c in kept if isinstance(c, Atom)]
|
|
284
|
+
growing = True
|
|
285
|
+
while growing:
|
|
286
|
+
growing = False
|
|
287
|
+
for atom in positive:
|
|
288
|
+
variables = atom.variables()
|
|
289
|
+
if variables & linked and not variables <= linked:
|
|
290
|
+
linked |= variables
|
|
291
|
+
growing = True
|
|
292
|
+
|
|
293
|
+
for conjunct in kept:
|
|
294
|
+
variables = conjunct.variables()
|
|
295
|
+
if not variables:
|
|
296
|
+
raise IlpEncodingError(
|
|
297
|
+
f"clause_to_formula: the goal {conjunct.to_unicode_str()!r} "
|
|
298
|
+
"has no variables, so it is decided against the whole fact "
|
|
299
|
+
"base rather than against one example — it cannot be part of "
|
|
300
|
+
"a formula about a single structure")
|
|
301
|
+
unlinked = variables - linked
|
|
302
|
+
if unlinked:
|
|
303
|
+
names = ", ".join(sorted(v.name for v in unlinked))
|
|
304
|
+
detail = (f"no {membership} atom is present at all"
|
|
305
|
+
if not seeds else
|
|
306
|
+
f"nothing connects {names} to the example")
|
|
307
|
+
raise IlpEncodingError(
|
|
308
|
+
f"clause_to_formula: the goal {conjunct.to_unicode_str()!r} "
|
|
309
|
+
f"is not linked to the example — {detail}. In Prolog such a "
|
|
310
|
+
"goal ranges over every example's facts at once, so it holds "
|
|
311
|
+
"globally; reading it as an ∃ over one structure would turn a "
|
|
312
|
+
"global claim into a local one. See unicode_logic_kit.ilp."
|
|
313
|
+
"readback's docstring.")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _is_membership(node: Node, wanted: str) -> bool:
|
|
317
|
+
return (isinstance(node, Atom) and node.predicate == wanted
|
|
318
|
+
and len(node.args) == 2)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _check_membership(atom: Atom, example: Variable, membership: str) -> None:
|
|
322
|
+
if atom.args[0] != example:
|
|
323
|
+
raise IlpEncodingError(
|
|
324
|
+
f"clause_to_formula: {atom.to_unicode_str()!r} attaches an "
|
|
325
|
+
f"individual to {atom.args[0].to_unicode_str()!r}, which is not "
|
|
326
|
+
f"the head's example {example.to_unicode_str()!r}. The clause "
|
|
327
|
+
"names a SECOND example and joins through it, so no formula about "
|
|
328
|
+
"one structure means the same thing — see unicode_logic_kit.ilp's "
|
|
329
|
+
"docstring for how this arises.")
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def hypothesis_to_formulas(text: str, *, membership: str = "atom_in",
|
|
333
|
+
predicates: Optional[Iterable[PredicateSpec]] = None,
|
|
334
|
+
negation_as_failure: str = "refuse",
|
|
335
|
+
target: Optional[str] = None) -> List[Node]:
|
|
336
|
+
"""Every clause of a learner's printed hypothesis, in order.
|
|
337
|
+
|
|
338
|
+
Comment lines — which is what a learner's precision/recall banner looks
|
|
339
|
+
like to a Prolog reader — are skipped by
|
|
340
|
+
:func:`~unicode_logic_kit.fol.parse_prolog_program`'s splitter.
|
|
341
|
+
|
|
342
|
+
The clauses come back **separately and are not disjoined**. Two clauses
|
|
343
|
+
with the same head are alternatives only under the COMPLETION of the
|
|
344
|
+
program, which is an assumption about the whole program rather than a fact
|
|
345
|
+
about those two clauses — the same line
|
|
346
|
+
:mod:`unicode_logic_kit.fol.prolog_input` draws. If the assumption holds for
|
|
347
|
+
your learner, ``functools.reduce(Or, formulas)`` is the definition it
|
|
348
|
+
licenses, and writing that yourself is the point.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
target: when given, refuse any clause whose head is not this
|
|
352
|
+
predicate. A learner that invented a helper predicate has produced
|
|
353
|
+
a program, not a definition, and silently reading only its first
|
|
354
|
+
clause would be wrong.
|
|
355
|
+
"""
|
|
356
|
+
formulas: List[Node] = []
|
|
357
|
+
for clause in _clauses_of(text):
|
|
358
|
+
if target is not None:
|
|
359
|
+
_check_head(clause, target, negation_as_failure)
|
|
360
|
+
formulas.append(clause_to_formula(
|
|
361
|
+
clause, membership=membership, predicates=predicates,
|
|
362
|
+
negation_as_failure=negation_as_failure))
|
|
363
|
+
return formulas
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _clauses_of(text: str) -> List[str]:
|
|
367
|
+
"""The clause texts, reusing the program splitter's comment handling."""
|
|
368
|
+
from ..fol.prolog_input import _split_clauses
|
|
369
|
+
|
|
370
|
+
return [c for c in _split_clauses(text) if c.strip()]
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _check_head(clause: str, target: str, negation_as_failure: str) -> None:
|
|
374
|
+
# The caller's negation_as_failure has to be forwarded here too: this is a
|
|
375
|
+
# SECOND parse of the same text, and parsing it under the default would
|
|
376
|
+
# refuse a `\+` clause before clause_to_formula ever saw the opt-in — and
|
|
377
|
+
# the refusal would tell the caller to pass the flag they just passed.
|
|
378
|
+
matrix = parse_prolog_clause(clause, mode="clause",
|
|
379
|
+
negation_as_failure=negation_as_failure)
|
|
380
|
+
while isinstance(matrix, Quantifier) and matrix.type in _FORALL:
|
|
381
|
+
matrix = matrix.formula
|
|
382
|
+
head = matrix.right if isinstance(matrix, Implies) else matrix
|
|
383
|
+
predicate = head.predicate if isinstance(head, Atom) else None
|
|
384
|
+
if predicate != _cap(target):
|
|
385
|
+
raise IlpEncodingError(
|
|
386
|
+
f"hypothesis_to_formulas: expected every clause to define "
|
|
387
|
+
f"{_cap(target)}, but {clause.strip()!r} defines {predicate!r}. A "
|
|
388
|
+
"hypothesis with invented helper predicates is a program, not one "
|
|
389
|
+
"definition; read it with parse_prolog_program instead.")
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Does this formula actually separate the two example sets?
|
|
2
|
+
|
|
3
|
+
The question to ask **before** running a learner, and again after. Before,
|
|
4
|
+
about the reference definition: if the axiom you believe in does not already
|
|
5
|
+
tell the positives from the negatives under the kit's own model checker, the
|
|
6
|
+
task is broken, and whatever the learner returns — perfect scores included —
|
|
7
|
+
would have meant nothing. After, about the learned clause: a hypothesis is
|
|
8
|
+
consistent with its examples *inside the learner*, over the ground facts the
|
|
9
|
+
learner was given; whether it still is over the structures those facts came
|
|
10
|
+
from is a different claim, and this is what checks it.
|
|
11
|
+
|
|
12
|
+
It is also the natural place to notice that a learner returns the SMALLEST
|
|
13
|
+
hypothesis consistent with the examples, so every property the negatives did
|
|
14
|
+
not force it to name is a hole. Run this over held-out structures and the
|
|
15
|
+
holes show up as false positives.
|
|
16
|
+
|
|
17
|
+
**Three ways not to be separated, kept apart.** A formula can fail to decide
|
|
18
|
+
an example because it is false there (a genuine misclassification), because
|
|
19
|
+
the step budget ran out (undecided — never counted as false, the same rule
|
|
20
|
+
:func:`~unicode_logic_kit.eval.check_definitions` follows), or because it names
|
|
21
|
+
a predicate the structure does not interpret (a vocabulary error, usually a
|
|
22
|
+
naming mismatch rather than anything logical). All three block separation, and
|
|
23
|
+
each is reported as itself.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from typing import Any, Dict, Optional, Tuple
|
|
28
|
+
|
|
29
|
+
from ..fol.nodes import Node
|
|
30
|
+
from ..semantics.model_eval import (
|
|
31
|
+
UninterpretedSymbol, UnsupportedNode, evaluate_detailed,
|
|
32
|
+
)
|
|
33
|
+
from .task import IlpTask
|
|
34
|
+
|
|
35
|
+
__all__ = ["SeparationReport", "check_separation"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class SeparationReport:
|
|
40
|
+
"""What :func:`check_separation` found, per example and in total."""
|
|
41
|
+
|
|
42
|
+
#: One dict per example, in task order, with keys ``id``, ``label``,
|
|
43
|
+
#: ``status`` (``"ok"`` / ``"exhausted"`` / ``"eval_error"``), ``holds``
|
|
44
|
+
#: (``None`` unless ``status == "ok"``), ``steps`` and ``error``.
|
|
45
|
+
rows: Tuple[Dict[str, Any], ...]
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def counts(self) -> Dict[str, int]:
|
|
49
|
+
"""True/false positives and negatives over the DECIDED rows, plus the
|
|
50
|
+
two undecided kinds."""
|
|
51
|
+
decided = [r for r in self.rows if r["status"] == "ok"]
|
|
52
|
+
return {
|
|
53
|
+
"true_positive": sum(1 for r in decided if r["label"] and r["holds"]),
|
|
54
|
+
"false_positive": sum(1 for r in decided
|
|
55
|
+
if not r["label"] and r["holds"]),
|
|
56
|
+
"true_negative": sum(1 for r in decided
|
|
57
|
+
if not r["label"] and not r["holds"]),
|
|
58
|
+
"false_negative": sum(1 for r in decided
|
|
59
|
+
if r["label"] and not r["holds"]),
|
|
60
|
+
"exhausted": sum(1 for r in self.rows if r["status"] == "exhausted"),
|
|
61
|
+
"eval_error": sum(1 for r in self.rows if r["status"] == "eval_error"),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def misclassified(self) -> Tuple[Dict[str, Any], ...]:
|
|
66
|
+
"""The rows the formula decided the wrong way. Undecided rows are NOT
|
|
67
|
+
in here — they are in :attr:`counts` under their own names, because
|
|
68
|
+
"could not tell" and "told me the opposite" call for different fixes."""
|
|
69
|
+
return tuple(r for r in self.rows
|
|
70
|
+
if r["status"] == "ok" and r["holds"] != r["label"])
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def separates(self) -> bool:
|
|
74
|
+
"""Every example decided, and decided as labelled.
|
|
75
|
+
|
|
76
|
+
``False`` for an empty report: ``all()`` over nothing is ``True``, and
|
|
77
|
+
answering "this formula separates the sets" about a report that
|
|
78
|
+
decided nothing is the kind of vacuous yes this kit does not hand out.
|
|
79
|
+
:func:`check_separation` cannot produce one (a task needs at least one
|
|
80
|
+
example of each label), but a caller filtering rows can.
|
|
81
|
+
|
|
82
|
+
>>> SeparationReport(()).separates
|
|
83
|
+
False
|
|
84
|
+
"""
|
|
85
|
+
return bool(self.rows) and all(
|
|
86
|
+
r["status"] == "ok" and r["holds"] == r["label"] for r in self.rows)
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def precision(self) -> Optional[float]:
|
|
90
|
+
"""Over the decided rows; ``None`` when the formula held nowhere —
|
|
91
|
+
never a fabricated ``0.0`` for an undefined ratio."""
|
|
92
|
+
counts = self.counts
|
|
93
|
+
predicted = counts["true_positive"] + counts["false_positive"]
|
|
94
|
+
return counts["true_positive"] / predicted if predicted else None
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def recall(self) -> Optional[float]:
|
|
98
|
+
"""Over the decided rows; ``None`` when there were no decided
|
|
99
|
+
positives."""
|
|
100
|
+
counts = self.counts
|
|
101
|
+
actual = counts["true_positive"] + counts["false_negative"]
|
|
102
|
+
return counts["true_positive"] / actual if actual else None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def check_separation(formula: Node, task: IlpTask, *,
|
|
106
|
+
all_different: bool = False,
|
|
107
|
+
budget: Optional[int] = None) -> SeparationReport:
|
|
108
|
+
"""Evaluate ``formula`` in every example structure of ``task``.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
formula: a parsed formula — ``MSFLParser().parse(text)`` for kit
|
|
112
|
+
syntax, :func:`~unicode_logic_kit.parse_tptp_formula` for TPTP, or
|
|
113
|
+
:func:`~unicode_logic_kit.ilp.clause_to_formula` for a learned
|
|
114
|
+
clause. Deliberately not a string: which dialect a string is in is
|
|
115
|
+
exactly the kind of guess this kit does not make.
|
|
116
|
+
task: supplies the structures and their labels.
|
|
117
|
+
all_different: require distinct variables to denote distinct
|
|
118
|
+
individuals — see
|
|
119
|
+
:mod:`unicode_logic_kit.semantics.model_eval`. A learner's clause
|
|
120
|
+
usually WANTS this: ``bDOUBLE(C,O) ∧ bSINGLE(C,N)`` with ``O`` and
|
|
121
|
+
``N`` allowed to coincide is a weaker pattern than the one the
|
|
122
|
+
clause looks like.
|
|
123
|
+
budget: evaluation step budget per example. ``None`` is unbounded.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
A :class:`SeparationReport`.
|
|
127
|
+
"""
|
|
128
|
+
if isinstance(formula, str):
|
|
129
|
+
raise TypeError(
|
|
130
|
+
"check_separation: formula must be a parsed Node, not text — "
|
|
131
|
+
"parse it first (MSFLParser().parse(...) for kit syntax, "
|
|
132
|
+
"parse_tptp_formula(...) for TPTP), so the dialect is your "
|
|
133
|
+
"decision and not a guess")
|
|
134
|
+
rows = []
|
|
135
|
+
for example in task.examples:
|
|
136
|
+
row: Dict[str, Any] = {"id": example.id, "label": example.label,
|
|
137
|
+
"status": "ok", "holds": None, "steps": None,
|
|
138
|
+
"error": None}
|
|
139
|
+
try:
|
|
140
|
+
result = evaluate_detailed(formula, example.structure,
|
|
141
|
+
all_different=all_different,
|
|
142
|
+
budget=budget)
|
|
143
|
+
except (UninterpretedSymbol, UnsupportedNode) as exc:
|
|
144
|
+
row["status"] = "eval_error"
|
|
145
|
+
row["error"] = f"{type(exc).__name__}: {exc}"
|
|
146
|
+
else:
|
|
147
|
+
row["steps"] = result.steps
|
|
148
|
+
if result.exhausted:
|
|
149
|
+
row["status"] = "exhausted"
|
|
150
|
+
else:
|
|
151
|
+
row["holds"] = result.holds
|
|
152
|
+
rows.append(row)
|
|
153
|
+
return SeparationReport(tuple(rows))
|