From cdd742c950d498e29182abe7f51cd506e4945bf9 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Sun, 21 Jun 2026 22:47:46 +0200 Subject: [PATCH] feat: bucket selection matching Klein 9B table Co-Authored-By: Claude Opus 4.8 --- gates/buckets.py | 31 +++++++++++++++++++++++++++++++ tests/test_buckets.py | 27 +++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 gates/buckets.py create mode 100644 tests/test_buckets.py diff --git a/gates/buckets.py b/gates/buckets.py new file mode 100644 index 0000000..8f1540a --- /dev/null +++ b/gates/buckets.py @@ -0,0 +1,31 @@ +"""Pure bucket math for KLEIN_BUCKET_SIZES.md. Stdlib only.""" +import math + + +def pick_bucket(iw, ih, resolution=1280, divisible=64): + """Choose the on-grid bucket (W,H), area <= resolution^2, nearest to the + image aspect (log distance; tie-break larger area).""" + budget = resolution * resolution + target = iw / ih + best = None + w = divisible + w_max = budget // divisible + while w <= w_max: + h = (budget // w // divisible) * divisible # largest on-grid h within budget + if h >= divisible: + err = abs(math.log(w / h) - math.log(target)) + cand = (err, -(w * h), w, h) # min err, then max area + if best is None or cand < best: + best = cand + w += divisible + return best[2], best[3] + + +def cover_crop_params(iw, ih, W, H): + """Cover-scale + centered crop to land (iw,ih) exactly on (W,H).""" + scale = max(W / iw, H / ih) + new_w = max(W, round(iw * scale)) + new_h = max(H, round(ih * scale)) + left = (new_w - W) // 2 + top = (new_h - H) // 2 + return new_w, new_h, left, top, scale diff --git a/tests/test_buckets.py b/tests/test_buckets.py new file mode 100644 index 0000000..47bf7eb --- /dev/null +++ b/tests/test_buckets.py @@ -0,0 +1,27 @@ +from gates import buckets + +# (iw, ih) -> expected (W, H) from KLEIN_BUCKET_SIZES.md, budget 1280, ÷64 +CASES = [ + (1000, 1000, 1280, 1280), # square + (1000, 2000, 896, 1792), # a=0.50 portrait + (1000, 1730, 960, 1664), # a≈0.58 + (1000, 1100, 1216, 1344), # a≈0.90 -> portrait-leaning + (2000, 1000, 1792, 896), # a=2.00 landscape + (1500, 1000, 1536, 1024), # a=1.50 +] + + +def test_pick_bucket_matches_table(): + for iw, ih, W, H in CASES: + assert buckets.pick_bucket(iw, ih, 1280, 64) == (W, H) + + +def test_buckets_are_on_grid_and_within_budget(): + for iw, ih, *_ in CASES: + W, H = buckets.pick_bucket(iw, ih, 1280, 64) + assert W % 64 == 0 and H % 64 == 0 + assert W * H <= 1280 * 1280 + + +def test_square_is_exactly_1280(): + assert buckets.pick_bucket(512, 512, 1280, 64) == (1280, 1280)