File size: 9,337 Bytes
6cb442b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Run: python test_packing.py   (or: python -m pytest -q)"""
from fractions import Fraction as F
import math
from packing import Sq, validate, parse_squares
from bounds import classify, find_conflicts

one, zero = F(1), F(0)


def axis(x, y):
    return Sq(F(x), F(y), one, zero)


def five(S):
    c, s = math.cos(math.pi / 4), math.sin(math.pi / 4)
    return [Sq(0.5, 0.5, 1.0, 0.0), Sq(S - 0.5, 0.5, 1.0, 0.0),
            Sq(0.5, S - 0.5, 1.0, 0.0), Sq(S - 0.5, S - 0.5, 1.0, 0.0), Sq(S / 2, S / 2, c, s)]


def test_user_cases():
    four = [axis(F(1, 2), F(1, 2)), axis(F(3, 2), F(1, 2)), axis(F(1, 2), F(3, 2)), axis(F(3, 2), F(3, 2))]
    r = validate(four, F(2))
    assert r["valid"] and r["mode"] == "exact" and r["proof"]
    # draft bug: axis(1, ...) passed int 1 -> draft fell into float mode; now stays exact
    r = validate([axis(F(1, 2), F(1, 2)), axis(1, F(1, 2))], F(2))
    assert not r["valid"] and r["max_overlap"] == 0.5 and r["mode"] == "exact"
    q = Sq(F(7, 10), F(7, 10), F(3, 5), F(4, 5))
    assert validate([q], F(7, 5))["valid"]
    assert not validate([q], F(7, 5) - F(1, 10**9))["valid"]
    S = 2 + math.sqrt(2) / 2
    r = validate(five(S), S)
    assert r["valid"] and not r["proof"] and r["mode"].startswith("float")
    assert not validate(five(2.70), 2.70)["valid"]


def test_classify_conflicts():
    ok = {"valid": True}
    assert classify(2.7, 2.7071, ok)[0] == "improvement"
    assert classify(2.7071, 2.7071, ok)[0] == "unchanged"
    assert classify(2.7, 2.7071, None)[0] == "unverified"
    assert classify(2.8, 2.7071, None)[0] == "worse"
    assert classify(None, 2.7071)[0] == "partial"
    assert classify(2.7, 2.7071, {"valid": False})[0] == "invalid"
    assert classify(7.5, None)[0] == "no_baseline"
    assert len(find_conflicts([{"n": 5, "s": 2.70, "source_url": "a"}, {"n": 5, "s": 2.71, "source_url": "b"}])) == 1
    assert find_conflicts([{"n": 5, "s": 2.7, "source_url": "a"}, {"n": 6, "s": 9.0, "source_url": "b"}]) == []


def test_edge_cases():
    # edge-touching side by side, exact: valid
    assert validate([axis(F(1, 2), F(1, 2)), axis(F(3, 2), F(1, 2))], F(2), expected_n=2)["valid"]
    # corner-touching diagonal: valid (skipped by dx^2+dy^2>2 prefilter? distance^2 = 2 -> checked, depth 0)
    assert validate([axis(F(1, 2), F(1, 2)), axis(F(3, 2), F(3, 2))], F(2))["valid"]
    # non-unit direction fails
    r = validate([Sq(F(1), F(1), F(3, 5), F(3, 5))], F(2))
    assert not r["valid"] and any("unit vector" in i for i in r["issues"])
    # expected_n mismatch fails
    r = validate([axis(F(1, 2), F(1, 2))], F(2), expected_n=2)
    assert not r["valid"] and "expected 2" in r["issues"][0]
    # sticking out
    assert not validate([axis(F(1, 2), F(1, 2))], F(9, 10))["valid"]
    # parse: exact strings and explicit degree unit
    sq = parse_squares([{"x": "7/10", "y": "7/10", "c": "3/5", "s": "4/5"}])
    assert validate(sq, F(7, 5))["proof"]
    sq = parse_squares([{"x": 0.5, "y": 0.5, "theta_deg": 90}])
    assert validate(sq, 1.0)["valid"]
    try:
        parse_squares([{"x": 0, "y": 0, "angle": 3}])
        assert False
    except ValueError:
        pass
    # sweep early exit does not hide an overlap after a far square in x-order
    sqs = [axis(F(1, 2), F(1, 2)), axis(F(1, 2), F(5, 2)), axis(F(1, 2), F(3, 4))]
    assert not validate(sqs, F(3))["valid"]



def test_thresholds():
    from bounds import IMPROVEMENT_MATERIAL, VALIDATION_EPS, classify
    assert IMPROVEMENT_MATERIAL > VALIDATION_EPS
    OK = {"valid": True}
    assert classify(2.0 - 5e-9, 2.0, OK)[0] == "unchanged"      # inside material band
    assert classify(2.0 - 2e-8, 2.0, OK)[0] == "improvement"
    assert classify(2.0 - 2e-8, 2.0, None)[0] == "unverified"


def test_display_precision():
    from tools import extract_claims
    cl, _ = extract_claims("s(5) = 2.70710678…", {"url": "u"})
    assert cl[0]["s_uncertainty"] == 1e-8
    cl, _ = extract_claims("s(68) ≤ 8.798795237218", {"url": "u"})
    assert cl and cl[0]["s"] == 8.798795237218 and cl[0]["s_uncertainty"] == 0


def test_llm_plumbing():
    from interfaces import LLMExtractor, MockLLMClient
    txt = "12 squares cannot fit below 3.9715. We found s(68) = 8.7987952 here."
    m = MockLLMClient([{"claims": [{"n": 12, "s": 3.9715, "kind": "lower_bound", "quote": "cannot fit below 3.9715"},
                                   {"n": 68, "s": 8.7987952, "kind": "upper_bound_packing", "quote": "s(68) = 8.7987952"},
                                   {"n": 29, "s": 5.9, "kind": "upper_bound_packing", "quote": "invented 5.9"}]}])
    cl, notes = LLMExtractor(m).extract(txt, {"url": "u"})
    assert [(c["n"], c["s"]) for c in cl] == [(68, 8.7987952)], cl
    cl, notes = LLMExtractor(MockLLMClient([RuntimeError("down")])).extract("s(68) = 8.7987952", {"url": "u"})
    assert cl and any("rule fallback" in x for x in notes)


def test_review_fixes():
    from interfaces import LLMExtractor, MockLLMClient, chunk_lines, side_from_quote, RegexGuardExtractor
    X = lambda items, text: LLMExtractor(MockLLMClient([{"claims": items}])).extract(text, {"url": "u"})[0]
    # 1a. model-returned s is ignored: rounded 5.9 can't sneak in; s comes from the quote
    cl = X([{"n": 29, "s": 5.9, "kind": "upper_bound_packing", "quote": "s(29) = 5.93383346"}], "Best: s(29) = 5.93383346 today.")
    assert [(c["n"], c["s"]) for c in cl] == [(29, 5.93383346)], cl
    # 1b. a quote that stops mid-number is re-read from the source: 5.9 -> 5.93383346, never 5.9
    cl = X([{"n": 29, "kind": "upper_bound_packing", "quote": "s(29) = 5.9"}], "s(29) = 5.93383346")
    assert [c["s"] for c in cl] == [5.93383346], cl
    # 1b'. a quote not present verbatim is dropped
    assert X([{"n": 29, "kind": "upper_bound_packing", "quote": "s(29) = 5.90"}], "s(29) = 5.93383346") == []
    # 1c. integer s: '3' inside '13' or '2.35' never matches; n itself isn't taken as s
    assert side_from_quote("n=13 needs 2.35 more", 13)[0] == "2.35"
    assert side_from_quote("4 squares fit in side 2", 4)[0] == "2"
    assert side_from_quote("n = 3", 3)[0] is None
    # 1d. ambiguous quote (two side values) is rejected
    assert side_from_quote("for 29 squares, 5.9338 or 5.9340", 29)[0] is None
    # 1e. hedged line rejected even if the model labels it upper_bound_packing
    assert X([{"n": 29, "kind": "upper_bound_packing", "quote": "s(29) = 5.90"}], "Our goal: s(29) = 5.90") == []
    # 1f. truncation marker outside the quote still sets uncertainty
    cl = X([{"n": 5, "kind": "upper_bound_packing", "quote": "s(5) = 2.70710678"}], "s(5) = 2.70710678...")
    assert cl and cl[0]["s_uncertainty"] == 1e-8
    # 4. chunker splits on lines, never mid-number, with overlap
    t = "\n".join(f"row {i}: s(29) = 5.93383346" for i in range(50))
    ch = chunk_lines(t, max_chars=200, overlap_lines=2)
    assert len(ch) > 1 and all(len(c) <= 200 for c in ch)
    assert all(l in t.splitlines() for c in ch for l in c.split("\n"))
    assert ch[0].split("\n")[-1] == ch[1].split("\n")[0] or ch[0].split("\n")[-2] == ch[1].split("\n")[0]
    assert "2.7071067811" in "".join(chunk_lines("x " * 30 + "2.7071067811", max_chars=40))
    # 5. middle arm: guards drop hedged and impossible values
    cl, _ = RegexGuardExtractor().extract("n = 5 target 2.6\nn = 5 gives 1.2\nn = 5 gives 2.7071067812", {"url": "u"})
    assert [(c["n"], c["s"]) for c in cl if not c.get("rejected")] == [(5, 2.7071067812)]
    # 7. float-validated claim just under the bound stays a candidate
    from bounds import float_near_bound_margin
    assert float_near_bound_margin(1000) > 1e-6


def test_float_near_bound():
    import json as _j
    from agent import Config, run as _run
    from bounds import Bound
    from tools import MockTools
    from make_scenarios import five as _five, S5 as _S5
    pg = "```packing\n" + _j.dumps(_five(_S5)) + "\n```"
    T = {"search": [{"url": "u", "date": "day-N"}], "pages": {"u": {"text": pg}}}
    r = _run({"n": [5]}, MockTools(T), {5: Bound(5, _S5 + 1.2e-8, "test", "", False)}, 8, Config())
    assert not r["improvements"] and [c["n"] for c in r["candidates"]] == [5], r["summary"]
    r = _run({"n": [5]}, MockTools(T), {5: Bound(5, _S5 + 1e-4, "test", "", False)}, 8, Config())
    assert [c["n"] for c in r["improvements"]] == [5]


def test_n_and_comma():
    from interfaces import LLMExtractor, MockLLMClient, n_in_quote
    X = lambda items, text: LLMExtractor(MockLLMClient([{"claims": items}])).extract(text, {"url": "u"})[0]
    assert X([{"n": 29, "kind": "upper_bound_packing", "quote": "side 2.70710678"}], "side 2.70710678 for something") == []
    t = "n = 10: container side of 3,70711"
    cl = X([{"n": 10, "kind": "upper_bound_packing", "quote": t}], t)
    assert [(c["n"], c["s"]) for c in cl] == [(10, 3.70711)], cl
    assert n_in_quote("s(29) = 5.93", 29) and not n_in_quote("2.135 units", 13)


if __name__ == "__main__":
    test_user_cases(); test_classify_conflicts(); test_edge_cases()
    print("all tests passed")
    test_thresholds(); test_display_precision(); test_llm_plumbing()
    print("threshold + llm-interface tests passed")
    test_review_fixes()
    print("review-fix tests passed")
    test_float_near_bound()
    print("float near-bound tests passed")
    test_n_and_comma()
    print("n_in_quote + comma-decimal tests passed")