unicode-logic-kit 0.31.0__py3-none-any.whl

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