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,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)
|