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