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,343 @@
1
+ """Predicate-aligned string matching for NL→FOL evaluation.
2
+
3
+ When scoring a model that translates natural language to FOL, two formulas may
4
+ denote the same thing while using *different predicate names* — a model might
5
+ write ``Wins(x)`` where the reference writes ``IsWinner(x)``. A plain string
6
+ comparison (or even a structural one) counts that as wrong, even though the
7
+ logical *shape* is identical and only the lexical choice of predicate symbol
8
+ differs. ``match_predicates`` closes that gap: it greedily renames each
9
+ predicate/function symbol in the prediction to the closest reference symbol
10
+ (by **normalised Levenshtein distance**, accepting a match at or below a
11
+ distance threshold) and returns the rewritten string, so a subsequent string
12
+ comparison rewards a structurally-correct answer that merely renamed its
13
+ predicates.
14
+
15
+ This is a deliberately **lexical / string-level** notion, complementary to the
16
+ AST-level :func:`unicode_logic_kit.eval.canonical.exact_match`:
17
+
18
+ * :func:`exact_match` (canonical) quotients out α-renaming, commutativity /
19
+ associativity, operand duplication, and double negation, but treats two
20
+ *different predicate names* as a genuine mismatch.
21
+ * :func:`match_predicates` / :func:`formulas_are_matched_identical` quotient out
22
+ *predicate-name* differences (and, via :func:`formulas_are_identical`,
23
+ whitespace and case), but not the structural rewrites above.
24
+
25
+ The two are orthogonal and are typically reported as separate metrics
26
+ (``EXACT_MATCH`` vs ``PREDICATE_MATCHED_EXACT_MATCH``). The matcher is
27
+ parser-independent: it operates directly on the surface strings, so it also
28
+ applies to raw model output that does not (yet) parse.
29
+
30
+ The Levenshtein distance is computed in pure Python (classical unit-cost
31
+ insertion / deletion / substitution dynamic program), so this module adds no
32
+ third-party dependency.
33
+ """
34
+
35
+ import re
36
+ from dataclasses import replace
37
+ from typing import Dict, FrozenSet, Tuple
38
+
39
+ from unicode_logic_kit.fol.nodes import (
40
+ Node, Atom, Function, Constant, SortedConstant,
41
+ )
42
+ # The lenient (non-raising) symbol classification walk itself — including
43
+ # which symbols are built-in operators rather than user vocabulary (=, ≠, <,
44
+ # … and +, -, *, /) — is shared with unicode_logic_kit.fol.signature; see
45
+ # _symbol_inventory's docstring below.
46
+ from unicode_logic_kit.fol.signature import inventory_of
47
+
48
+ __all__ = [
49
+ "formulas_are_identical",
50
+ "match_predicates",
51
+ "formulas_are_matched_identical",
52
+ "align_symbols",
53
+ "aligned_exact_match",
54
+ ]
55
+
56
+ # A predicate or function symbol is a maximal word immediately followed by an
57
+ # opening parenthesis, e.g. the ``P`` in ``P(x)`` or the ``loves`` in
58
+ # ``loves(a, b)``. Nullary predicates written without parentheses are not
59
+ # captured (there is nothing lexical to realign), and neither are bare terms.
60
+ _SYMBOL_BEFORE_PAREN = re.compile(r"\b\w+(?=\()")
61
+
62
+ _WHITESPACE = re.compile(r"\s+")
63
+
64
+
65
+ def _levenshtein(a: str, b: str) -> int:
66
+ """Return the Levenshtein edit distance between ``a`` and ``b``.
67
+
68
+ Classical unit-cost dynamic program (insertion, deletion, and substitution
69
+ each cost 1), computed with a single rolling row in O(len(a)·len(b)) time
70
+ and O(len(b)) space. Matches the value of ``Levenshtein.distance`` for the
71
+ same inputs, so results are identical whether or not the optional
72
+ ``python-Levenshtein`` C extension is installed.
73
+ """
74
+ if a == b:
75
+ return 0
76
+ if not a:
77
+ return len(b)
78
+ if not b:
79
+ return len(a)
80
+
81
+ previous = list(range(len(b) + 1))
82
+ for i, ca in enumerate(a, start=1):
83
+ current = [i]
84
+ for j, cb in enumerate(b, start=1):
85
+ insertion = current[j - 1] + 1
86
+ deletion = previous[j] + 1
87
+ substitution = previous[j - 1] + (ca != cb)
88
+ current.append(min(insertion, deletion, substitution))
89
+ previous = current
90
+ return previous[len(b)]
91
+
92
+
93
+ def _normalised_distance(a: str, b: str) -> float:
94
+ """Levenshtein distance scaled by the longer string's length, in [0, 1].
95
+
96
+ Normalising by ``max(len(a), len(b))`` makes the threshold length-agnostic:
97
+ a one-character edit weighs more between two short names than between two
98
+ long ones. Both names are predicate/function symbols matched by
99
+ :data:`_SYMBOL_BEFORE_PAREN`, hence always non-empty, so the denominator is
100
+ never zero.
101
+ """
102
+ return _levenshtein(a, b) / max(len(a), len(b))
103
+
104
+
105
+ def formulas_are_identical(prediction: str, reference: str) -> bool:
106
+ """Return whether two formula strings are equal ignoring whitespace and case.
107
+
108
+ Both strings are stripped of all whitespace and lower-cased before
109
+ comparison, so ``"∀x P(x)"`` and ``"∀x p( x )"`` are considered identical.
110
+ This is the plain ``EXACT_MATCH`` notion; it does **not** realign predicate
111
+ names — use :func:`formulas_are_matched_identical` for that.
112
+ """
113
+ cleaned_prediction = _WHITESPACE.sub("", prediction).lower()
114
+ cleaned_reference = _WHITESPACE.sub("", reference).lower()
115
+ return cleaned_prediction == cleaned_reference
116
+
117
+
118
+ def _map_predicates(
119
+ prediction_symbols: list,
120
+ reference_symbols: list,
121
+ max_norm_distance: float = 0.6,
122
+ ) -> list:
123
+ """Map each prediction symbol to its nearest reference symbol, or keep it.
124
+
125
+ For every symbol in ``prediction_symbols`` the closest symbol in
126
+ ``reference_symbols`` (smallest normalised Levenshtein distance) is found.
127
+ If that distance is at or below ``max_norm_distance`` the reference symbol is
128
+ used; otherwise the original prediction symbol is kept unchanged (the match
129
+ is too weak to trust). Ties are broken by the reference symbol's position,
130
+ matching ``min``'s first-minimum semantics.
131
+ """
132
+ mapped = []
133
+ for symbol in prediction_symbols:
134
+ best_match = min(
135
+ reference_symbols,
136
+ key=lambda candidate: _normalised_distance(symbol, candidate),
137
+ )
138
+ if _normalised_distance(symbol, best_match) <= max_norm_distance:
139
+ mapped.append(best_match)
140
+ else:
141
+ mapped.append(symbol)
142
+ return mapped
143
+
144
+
145
+ def match_predicates(
146
+ prediction: str,
147
+ reference: str,
148
+ max_norm_distance: float = 0.6,
149
+ ) -> str:
150
+ """Rewrite ``prediction``'s predicate/function names toward ``reference``.
151
+
152
+ Every symbol that appears immediately before a ``(`` in ``prediction`` is
153
+ realigned to the lexically-closest such symbol in ``reference`` (see
154
+ :func:`_map_predicates`), and the rewrite is applied to the surface string
155
+ as a ``"<old>(" → "<new>("`` substitution. Symbols with no sufficiently
156
+ close reference counterpart (normalised distance above ``max_norm_distance``)
157
+ are left as they are. If either side has no parenthesised symbols, the
158
+ prediction is returned unchanged.
159
+
160
+ The result is a string in the same surface syntax as the input, suitable for
161
+ a subsequent :func:`formulas_are_identical` comparison or for re-parsing.
162
+ """
163
+ matched_formula = prediction
164
+ prediction_symbols = _SYMBOL_BEFORE_PAREN.findall(prediction)
165
+ reference_symbols = _SYMBOL_BEFORE_PAREN.findall(reference)
166
+
167
+ if prediction_symbols and reference_symbols:
168
+ mapped_symbols = _map_predicates(
169
+ prediction_symbols, reference_symbols, max_norm_distance
170
+ )
171
+ for old_symbol, new_symbol in zip(prediction_symbols, mapped_symbols):
172
+ matched_formula = matched_formula.replace(
173
+ old_symbol + "(", new_symbol + "("
174
+ )
175
+
176
+ return matched_formula
177
+
178
+
179
+ def formulas_are_matched_identical(
180
+ prediction: str,
181
+ reference: str,
182
+ max_norm_distance: float = 0.6,
183
+ ) -> bool:
184
+ """Return whether ``prediction`` equals ``reference`` after predicate realignment.
185
+
186
+ Realigns the prediction's predicate/function names to the reference's with
187
+ :func:`match_predicates`, then compares with :func:`formulas_are_identical`
188
+ (whitespace- and case-insensitive). This is the ``PREDICATE_MATCHED_EXACT``
189
+ notion: it forgives a structurally-correct answer that merely chose different
190
+ predicate symbol names.
191
+ """
192
+ matched_prediction = match_predicates(prediction, reference, max_norm_distance)
193
+ return formulas_are_identical(matched_prediction, reference)
194
+
195
+
196
+ # ---------------------------------------------------------------------------
197
+ # AST-level symbol alignment: separate namespaces, arity-aware, injective.
198
+ # ---------------------------------------------------------------------------
199
+ #
200
+ # The lexical matcher above deliberately works on surface strings (so it also
201
+ # applies to output that does not parse), but that comes with three known
202
+ # blind spots: predicates and functions share one regex, arity is ignored, and
203
+ # the greedy per-occurrence mapping can collapse two distinct prediction
204
+ # symbols onto the same reference symbol — which CHANGES the logical content.
205
+ # ``align_symbols`` is the parsed-AST counterpart without those blind spots.
206
+
207
+ # A predicate/function symbol key: (name, arity). Constants key on the name.
208
+ _SymKey = Tuple[str, int]
209
+
210
+
211
+ # _symbol_inventory: collects the user vocabulary of a Node per namespace —
212
+ # (preds, funcs, consts), where preds/funcs are sets of (name, arity) keys
213
+ # (built-in operators excluded, mirroring validate's classification) and
214
+ # consts is a set of constant names (Constant and SortedConstant — a sorted
215
+ # constant is renamed in place, its sort annotation untouched). Variables are
216
+ # deliberately NOT collected: bound-variable naming is canonicalize's job
217
+ # (α-renaming), not a vocabulary difference.
218
+ #
219
+ # This is a genuine alias — not a wrapper that happens to agree — for
220
+ # :func:`unicode_logic_kit.fol.signature.inventory_of`: the walk itself
221
+ # (including which symbols are built-in operators rather than user
222
+ # vocabulary) is tested and maintained in that one place rather than
223
+ # duplicated here (see that module's DESIGN NOTE). The name stays
224
+ # ``_symbol_inventory`` so existing importers (e.g. ``eval.equivalence``)
225
+ # keep working unchanged. Deliberately NEVER raises, unlike
226
+ # :meth:`~unicode_logic_kit.fol.signature.Signature.from_formulas` — see
227
+ # :func:`inventory_of`'s own docstring for exactly what it tolerates that
228
+ # ``from_formulas`` would refuse (conflicting arities, a name used both as a
229
+ # constant and as a function).
230
+ _symbol_inventory = inventory_of
231
+
232
+
233
+ def _greedy_injective(pred_keys, ref_keys, taken_names: FrozenSet[str],
234
+ max_norm_distance: float) -> Dict:
235
+ """Best-first injective assignment prediction-key → reference-key.
236
+
237
+ Candidate pairs are restricted to EQUAL ARITY (for ``(name, arity)`` keys;
238
+ plain constant names always pair), filtered by the normalised-Levenshtein
239
+ threshold, and sorted by (distance, names) for determinism. Each
240
+ prediction key maps to at most one reference key and vice versa
241
+ (injectivity: two distinct prediction symbols are never merged, which
242
+ would change the logical content). Identity pairs have distance 0, so a
243
+ symbol present on both sides always claims itself first and cannot be
244
+ captured by a near-miss neighbour.
245
+
246
+ ``taken_names`` are the names the prediction already uses in this
247
+ namespace: a key is never renamed INTO one of them (identity excepted) —
248
+ otherwise the rewrite could manufacture a name clash the prediction never
249
+ had (e.g. renaming ``Foo/2`` to ``Fo`` while the prediction already uses
250
+ ``Fo/1`` would create a mixed-arity symbol out of thin air).
251
+ """
252
+ def name_of(key):
253
+ return key if isinstance(key, str) else key[0]
254
+
255
+ def arity_of(key):
256
+ return None if isinstance(key, str) else key[1]
257
+
258
+ pairs = []
259
+ for p in pred_keys:
260
+ for r in ref_keys:
261
+ if arity_of(p) != arity_of(r):
262
+ continue
263
+ if name_of(r) in taken_names and r != p:
264
+ continue
265
+ d = _normalised_distance(name_of(p), name_of(r))
266
+ if d <= max_norm_distance:
267
+ pairs.append((d, name_of(p), name_of(r), p, r))
268
+ pairs.sort(key=lambda t: (t[0], t[1], t[2]))
269
+
270
+ mapping: Dict = {}
271
+ used_targets: set = set()
272
+ for _d, _pn, _rn, p, r in pairs:
273
+ if p in mapping or r in used_targets:
274
+ continue
275
+ mapping[p] = r
276
+ used_targets.add(r)
277
+ # Identity entries are no-ops for the rewrite — drop them.
278
+ return {p: r for p, r in mapping.items() if p != r}
279
+
280
+
281
+ def align_symbols(prediction: Node, reference: Node,
282
+ max_norm_distance: float = 0.6) -> Node:
283
+ """Rename ``prediction``'s vocabulary toward ``reference``, AST-safely.
284
+
285
+ The parsed-AST counterpart of :func:`match_predicates` with three
286
+ guarantees the lexical matcher cannot give:
287
+
288
+ * **separate namespaces** — predicate, function, and constant symbols are
289
+ aligned independently (a predicate ``foo`` never interferes with a
290
+ function ``foo``);
291
+ * **arity-aware** — ``P/1`` is only ever aligned to a reference symbol of
292
+ arity 1;
293
+ * **injective + capture-free** — two distinct prediction symbols are never
294
+ merged onto one reference symbol, and a symbol is never renamed into a
295
+ name the prediction already uses. The result is the image of the
296
+ prediction under an injective renaming of its vocabulary, so its logical
297
+ content is preserved up to that renaming — the alignment can make a
298
+ formula *equal* to the reference, never *more true*.
299
+
300
+ Bound variables are untouched (α-renaming is ``canonicalize``'s job).
301
+ The distance measure and default threshold match :func:`match_predicates`.
302
+ """
303
+ p_preds, p_funcs, p_consts = _symbol_inventory(prediction)
304
+ r_preds, r_funcs, r_consts = _symbol_inventory(reference)
305
+
306
+ pred_map = _greedy_injective(
307
+ p_preds, r_preds, frozenset(n for n, _a in p_preds), max_norm_distance)
308
+ func_map = _greedy_injective(
309
+ p_funcs, r_funcs, frozenset(n for n, _a in p_funcs), max_norm_distance)
310
+ const_map = _greedy_injective(
311
+ p_consts, r_consts, frozenset(p_consts), max_norm_distance)
312
+
313
+ def rec(n: Node) -> Node:
314
+ n = n.map_children(rec)
315
+ if isinstance(n, Atom):
316
+ key = (n.predicate, len(n.args))
317
+ if key in pred_map:
318
+ return replace(n, predicate=pred_map[key][0])
319
+ elif isinstance(n, Function):
320
+ key = (n.name, len(n.args))
321
+ if key in func_map:
322
+ return replace(n, name=func_map[key][0])
323
+ elif isinstance(n, (Constant, SortedConstant)):
324
+ if n.name in const_map:
325
+ return replace(n, name=const_map[n.name])
326
+ return n
327
+
328
+ return rec(prediction)
329
+
330
+
331
+ def aligned_exact_match(prediction: Node, reference: Node,
332
+ max_norm_distance: float = 0.6) -> bool:
333
+ """Canonical exact match after AST-level symbol alignment.
334
+
335
+ Combines the two orthogonal quotients: :func:`align_symbols` forgives
336
+ lexical vocabulary differences (namespace- and arity-aware), then
337
+ :func:`unicode_logic_kit.eval.canonical.exact_match` forgives α-renaming,
338
+ commutativity/associativity, operand duplication, and double negation.
339
+ """
340
+ from .canonical import exact_match
341
+
342
+ return exact_match(align_symbols(prediction, reference, max_norm_distance),
343
+ reference)