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