File size: 9,917 Bytes
201dc77 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 | """Decontamination heuristics: behaviour on hand-built pairs (renamed binders, reordered hypotheses, numeral
types, unrelated statements). The expectations here ARE the measured false-positive / false-negative profile."""
from __future__ import annotations
import numpy as np
from align.data.decontam import (
ContaminationLedger, Decontaminator, EvalItem, Hit, LSHIndex, MinHasher, Params, lean_tokens, nl_tokens,
ngram_set, shingles, text_hash,
)
H = "import Mathlib\nimport Aesop\n\nset_option maxHeartbeats 0\n\nopen BigOperators Real Nat Topology Rat\n\n"
BASE = H + """/-- Let x and y be reals with x + y = 10 and x - y = 2. Prove that the product x * y equals 24 given that both
are positive and their sum of squares is 52. -/
theorem base_problem (x y : ℝ) (h₀ : 0 < x ∧ 0 < y) (h₁ : x + y = 10) (h₂ : x - y = 2) (h₃ : x ^ 2 + y ^ 2 = 52) :
x * y = 24 := by sorry"""
import re as _re
def _rename(src, mp):
return _re.sub(r"(?<![\w'])(" + "|".join(_re.escape(k) for k in mp) + r")(?![\w'])", lambda m: mp[m.group(0)], src)
RENAMED = _rename(BASE, {"base_problem": "other_name", "x": "a", "y": "b", "h₀": "p", "h₁": "q", "h₂": "r", "h₃": "s"})
EVAL_OTHER = H + "theorem other_eval (n : ℕ) (hn : 3 < n) (hm : n < 100) (hp : n.Prime) : ¬ (n % 6 = 0) := by sorry"
REORDERED = H + """theorem base_problem (x y : ℝ) (h₃ : x ^ 2 + y ^ 2 = 52) (h₂ : x - y = 2) (h₁ : x + y = 10) (h₀ : 0 < x ∧ 0 < y) :
x * y = 24 := by sorry"""
NUMERAL = BASE.replace("(x y : ℝ)", "(x y : ℕ)").replace("= 24", "= (24 : ℕ)")
CHANGED_CONST = BASE.replace("= 52", "= 58").replace("= 24", "= 21")
UNRELATED = H + """theorem unrelated (n : ℕ) (hn : 0 < n) (h : n % 7 = 3) : (n ^ 3 + 2 * n + 5) % 7 = 3 := by sorry"""
SHORT_A = H + "theorem a (x : ℝ) : x = x := by sorry"
SHORT_B = H + "theorem b (y : ℝ) : y = y := by sorry"
def dec(statement=BASE, informal=None, **kw):
return Decontaminator([EvalItem("E", "evalset", statement, informal)], Params(**kw) if kw else None)
def stages(d, stmt, informal=None):
return {h.stage for h in d.check(stmt, informal)}
# ---------------------------------------------------------------- minhash
def test_minhash_estimates_jaccard():
mh = MinHasher(num_perm=256, seed=3)
a = {f"s{i}" for i in range(200)}
b = {f"s{i}" for i in range(100, 300)} # |A∩B|=100, |A∪B|=300, J=1/3
sa = mh.signature(np.array([hash_(x) for x in a], dtype=np.uint64))
sb = mh.signature(np.array([hash_(x) for x in b], dtype=np.uint64))
assert abs(MinHasher.jaccard(sa, sb) - 1 / 3) < 0.09
assert MinHasher.jaccard(sa, sa) == 1.0
def hash_(s):
import zlib
return zlib.crc32(s.encode())
def test_lsh_recalls_similar_not_disjoint():
mh = MinHasher()
idx = LSHIndex(mh, bands=32, threshold=0.5)
base = [f"t{i}" for i in range(60)]
idx.add("base", mh.signature(shingles(base, 3)))
near = base[:55] + ["x1", "x2", "x3", "x4", "x5"]
far = [f"z{i}" for i in range(60)]
assert [i for i, _ in idx.query(mh.signature(shingles(near, 3)))] == ["base"]
assert idx.query(mh.signature(shingles(far, 3))) == []
assert idx.query(None) == []
def test_signature_is_deterministic_across_instances():
a = MinHasher(seed=1).signature(shingles(list("abcdefgh"), 3))
b = MinHasher(seed=1).signature(shingles(list("abcdefgh"), 3))
assert (a == b).all()
# ---------------------------------------------------------------- statement stages: measured profile
def test_identical_hits_every_exact_stage():
s = stages(dec(), BASE)
assert {"text_hash", "sig_sha", "alpha_sha", "minhash_lsh", "ngram_lean"} <= s
def test_renamed_theorem_name_and_comment_only_is_text_hash_hit():
s = stages(dec(), BASE.replace("base_problem", "renamed_thm").replace("Let x and y", "Suppose x and y"))
assert "text_hash" in s and "sig_sha" in s
def test_renamed_binders_caught_by_alpha_not_by_exact_hashes():
s = stages(dec(), RENAMED)
assert "alpha_sha" in s
assert "sig_sha" not in s and "text_hash" not in s # exact stages are blind to renaming (by design)
assert "minhash_lsh" in s # alpha-renamed tokens make the fuzzy stages invariant too
def test_reordered_hypotheses_missed_by_alpha_caught_by_fuzzy():
s = stages(dec(), REORDERED)
assert "alpha_sha" not in s and "text_hash" not in s # known false negative of the hash stages
assert "minhash_lsh" in s or "ngram_lean" in s # the fuzzy stages recover it
def test_numeral_type_variant_is_flagged_by_fuzzy_stages_but_not_by_hashes():
s = stages(dec(), NUMERAL)
assert not ({"text_hash", "sig_sha", "alpha_sha"} & s) # different statement: exact stages correctly differ
assert "minhash_lsh" in s or "ngram_lean" in s # over-flagged on purpose (documented false positive)
def test_changed_constants_not_flagged_exactly():
s = stages(dec(), CHANGED_CONST)
assert not ({"text_hash", "sig_sha", "alpha_sha"} & s)
def test_unrelated_statement_no_hit_at_all():
assert dec().check(UNRELATED) == []
def test_short_generic_statements_collide_only_in_alpha_hash():
"""`x = x` vs `y = y` is the same problem up to renaming (alpha_sha says so, correctly); the fuzzy stages are
disabled below `min_lean_tokens` precisely because tiny statements all look alike."""
d = dec(SHORT_A)
s = stages(d, SHORT_B)
assert "alpha_sha" in s
assert "minhash_lsh" not in s and "ngram_lean" not in s
def test_alpha_sha_conservative_false_positive_documented():
a = H + "theorem a (x : ℝ) (h : Real.sin x = 1) : x ≠ 0 := by sorry"
b = H + "theorem b (y : ℝ) (h : Real.cos y = 1) : y ≠ 0 := by sorry"
# p2_common keeps `sin`/`cos` lowercase-dotted as distinct tokens: the dotted names are NOT renamed
assert "alpha_sha" not in stages(dec(a), b)
def test_lean_tokens_alpha_invariance():
assert lean_tokens(BASE) == lean_tokens(RENAMED)
assert lean_tokens(BASE, alpha=False) != lean_tokens(RENAMED, alpha=False)
def test_text_hash_ignores_comments_header_name():
assert text_hash(BASE) == text_hash(BASE.replace("base_problem", "zzz").replace("Let x", "Take x"))
assert text_hash(BASE) != text_hash(CHANGED_CONST)
# ---------------------------------------------------------------- informal text
INFORMAL = ("Let x and y be real numbers such that x plus y equals ten and x minus y equals two, "
"find the product of x and y when both are positive and their squares sum to fifty two.")
def test_informal_ngram_catches_embedded_copy_and_hash_catches_exact():
d = dec(EVAL_OTHER, INFORMAL)
cand_informal = "Problem 7. " + INFORMAL + " Show your work."
s = stages(d, UNRELATED.replace("unrelated", "q"), cand_informal)
assert "ngram_nl" in s
assert "informal_hash" in stages(d, UNRELATED.replace("unrelated", "q"), INFORMAL)
def test_informal_paraphrase_below_ngram_is_missed_known_false_negative():
d = dec(EVAL_OTHER, INFORMAL)
para = "Two positive reals have sum 10 and difference 2; compute their product given the sum of squares is 52."
assert "ngram_nl" not in stages(d, UNRELATED.replace("unrelated", "q"), para)
def test_informal_unrelated_no_hit():
d = dec(EVAL_OTHER, INFORMAL)
assert stages(d, UNRELATED.replace("unrelated", "q"), "How many ways can five people sit around a circular table?") == set()
def test_docstring_of_eval_statement_counts_as_informal():
d = dec(BASE, None) # BASE's docstring is the informal text
cand = "Let x and y be reals with x + y = 10 and x - y = 2. Prove that the product x * y equals 24 given that both"
assert "ngram_nl" in stages(d, UNRELATED.replace("unrelated", "q"), cand)
def test_ngram_nl_ignores_formula_streams_and_stock_phrases():
d = dec(EVAL_OTHER, "Show that there exists a positive integer n such that a 2 b 2 b 2 c 2 c 2 d 2 holds for all values.")
cand = UNRELATED.replace("unrelated", "q")
assert "ngram_nl" not in stages(d, cand, "Prove: show that there exists a positive integer n such that the claim is trivial")
assert "ngram_nl" not in stages(d, cand, "We expand a 2 b 2 b 2 c 2 c 2 d 2 by hand in this example")
def test_nl_helpers():
assert nl_tokens("A, b-C 12!") == ["a", "b", "c", "12"]
assert len(ngram_set(list("abcdef"), 5)) == 2 and ngram_set(list("abc"), 5) == set()
# ---------------------------------------------------------------- elab hash + ancestry
def test_elab_hash_stage():
d = Decontaminator([EvalItem("E", "s", UNRELATED, elab_sha="deadbeef")])
assert [h.stage for h in d.check("theorem z : True := by sorry", elab_sha="deadbeef")] == ["elab_hash"]
assert d.check("theorem z : True := by sorry", elab_sha="other") == []
def test_ancestry_child_inherits_parent_contamination():
led = ContaminationLedger()
led.add("parent", [Hit("alpha_sha", "E")], {"root": "parent", "parents": []})
led.add("child", [], {"root": "parent", "parents": ["parent"]})
led.add("grandchild", [], {"root": "parent", "parents": ["child"]})
led.add("other", [], {"root": "other", "parents": []})
assert led.resolve("child") == (True, "inherited:parent")
assert led.resolve("grandchild")[0] is True
assert led.resolve("other") == (False, "clean")
assert led.resolve("parent") == (True, "direct:alpha_sha")
def test_ancestry_unknown_parent_is_reported_not_assumed_clean():
led = ContaminationLedger()
led.add("child", [], {"root": "ghost", "parents": ["ghost"]})
assert led.resolve("child") == (False, "unknown_parent")
led2 = ContaminationLedger(known={"ghost": True})
led2.add("child", [], {"root": "ghost", "parents": ["ghost"]})
assert led2.resolve("child")[0] is True
def test_ancestry_cycle_terminates():
led = ContaminationLedger()
led.add("a", [], {"parents": ["b"]})
led.add("b", [], {"parents": ["a"]})
assert led.resolve("a")[0] is False
|