FA-95056 / Warehouse slotting and bin packing / Open access
FEFO putaway: largest empty bin chosen · case 01
Small receipts consume the largest empty bins far from the pick face.
ROOT CAUSE
Empty bins are ranked by capacity descending instead of distance.
VERIFIED REPAIR
Choose the nearest empty bin that can hold the receipt, then bin id.
Unsuccessful approach: Ranking empty bins by id alone ignores travel distance.
Case contract
bins [[bin, sku|None, lot, expiry_day, qty, capacity, distance]]; receipt [sku, lot, expiry_day, qty]. A bin is empty when sku is None or qty is 0. Preference: (1) non-empty bin with same sku and lot and room (capacity-qty >= receipt qty), nearest then id; (2) same sku, different lot, expiry within 30 days either way, room; smallest expiry gap, nearest, id; (3) empty bin with capacity >= qty, nearest then id; else "HOLD".
Why this case matters
FEFO storage keeps lots with similar expiry together so the earliest expiring stock ships first.
1 / The failure
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(bins, receipt):
sku, lot, exp, q = receipt
def room(b):
return b[5] - b[4] >= q
def empty(b):
return b[1] is None or b[4] == 0
same_lot = [b for b in bins if not empty(b) and b[1] == sku and b[2] == lot and room(b)]
if same_lot:
return min(same_lot, key=lambda b: (b[6], b[0]))[0]
mixed = [b for b in bins if not empty(b) and b[1] == sku and b[2] != lot and abs(b[3] - exp) <= 30 and room(b)]
if mixed:
return min(mixed, key=lambda b: (abs(b[3] - exp), b[6], b[0]))[0]
free = [b for b in bins if empty(b) and b[5] >= q]
if free:
return max(free, key=lambda b: (b[5], b[0]))[0]
return 'HOLD'
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['generated mix 5', [[['B0', 'S1', 'L2', 131, 5, 20, 3], ['B1', None, None, 0, 0, 20, 3], ['B2', None, None, 0, 0, 40, 3], ['B3', None, None, 0, 0, 40, 1], ['B4', None, None, 0, 0, 40, 8], ['B5', None, None, 0, 0, 20, 3], ['B6', 'S2', 'L3', 100, 10, 40, 7]], ['S2', 'L2', 150, 20]], 'B3'], ['generated mix 3', [[['B0', None, None, 0, 0, 60, 5], ['B1', None, None, 0, 0, 60, 4], ['B2', 'S1', 'L1', 145, 30, 60, 2], ['B3', 'S2', 'L2', 100, 40, 20, 1], ['B4', 'S2', 'L1', 131, 30, 40, 2], ['B5', 'S1', 'L1', 145, 30, 40, 7]], ['S1', 'L4', 100, 10]], 'B1'], ['same lot preferred', [[['A', 'S1', 'L1', 100, 10, 40, 9], ['B', 'S1', 'L2', 100, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['residual label empty', [[['R', 'S9', 'L7', 50, 0, 60, 2], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'R'], ['nothing', [[['X', 'S2', 'L1', 100, 5, 10, 1]], ['S1', 'L1', 100, 20]], 'HOLD'], ['room exact', [[['A', 'S1', 'L1', 100, 20, 40, 9], ['E', None, None, 0, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['generated mix 8', [[['B0', 'S1', 'L2', 100, 30, 20, 6], ['B1', 'S2', 'L3', 131, 40, 40, 8], ['B2', None, None, 0, 0, 20, 4], ['B3', 'S2', 'L1', 145, 30, 40, 1], ['B4', 'S2', 'L3', 130, 0, 60, 6]], ['S1', 'L1', 130, 20]], 'B2']], [['generated mix 31', [[['B0', 'S1', 'L1', 145, 5, 60, 4], ['B1', None, None, 0, 0, 60, 7], ['B2', 'S1', 'L3', 160, 10, 60, 7], ['B3', 'S1', 'L1', 160, 0, 60, 8]], ['S1', 'L2', 100, 10]], 'B1'], ['generated mix 24', [[['B0', 'S1', 'L1', 131, 10, 40, 3], ['B1', 'S1', 'L2', 131, 5, 40, 9], ['B2', None, None, 0, 0, 60, 9], ['B3', 'S2', 'L1', 120, 40, 40, 8], ['B4', None, None, 0, 0, 60, 8], ['B5', 'S1', 'L3', 160, 0, 40, 6]], ['S2', 'L1', 130, 30]], 'B5'], ['expiry 31 days', [[['A', 'S1', 'L2', 131, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'E'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['same lot preferred', [[['A', 'S1', 'L1', 100, 10, 40, 9], ['B', 'S1', 'L2', 100, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['generated mix 9', [[['B0', 'S1', 'L2', 160, 40, 40, 1], ['B1', 'S1', 'L1', 131, 40, 40, 1], ['B2', 'S1', 'L3', 145, 0, 60, 1], ['B3', 'S1', 'L2', 100, 10, 20, 7], ['B4', 'S2', 'L1', 100, 30, 60, 5]], ['S1', 'L4', 150, 20]], 'B2'], ['generated mix 17', [[['B0', 'S2', 'L2', 120, 0, 20, 8], ['B1', 'S1', 'L2', 131, 5, 40, 3], ['B2', 'S2', 'L2', 100, 10, 20, 8], ['B3', 'S2', 'L2', 100, 10, 60, 6], ['B4', 'S1', 'L3', 100, 30, 40, 3], ['B5', 'S1', 'L1', 145, 10, 60, 5]], ['S1', 'L4', 150, 20]], 'B5']], [['generated mix 8', [[['B0', 'S1', 'L2', 100, 30, 20, 6], ['B1', 'S2', 'L3', 131, 40, 40, 8], ['B2', None, None, 0, 0, 20, 4], ['B3', 'S2', 'L1', 145, 30, 40, 1], ['B4', 'S2', 'L3', 130, 0, 60, 6]], ['S1', 'L1', 130, 20]], 'B2'], ['generated mix 71', [[['B0', 'S1', 'L3', 160, 5, 40, 5], ['B1', 'S1', 'L1', 130, 0, 60, 7], ['B2', 'S1', 'L3', 120, 0, 60, 2]], ['S2', 'L1', 100, 10]], 'B2'], ['nothing', [[['X', 'S2', 'L1', 100, 5, 10, 1]], ['S1', 'L1', 100, 20]], 'HOLD'], ['room exact', [[['A', 'S1', 'L1', 100, 20, 40, 9], ['E', None, None, 0, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 31 days', [[['A', 'S1', 'L2', 131, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'E'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['generated mix 21', [[['B0', 'S1', 'L3', 130, 40, 60, 3], ['B1', 'S1', 'L3', 145, 30, 40, 7], ['B2', 'S1', 'L2', 130, 40, 60, 4], ['B3', 'S2', 'L2', 120, 30, 40, 3], ['B4', 'S1', 'L3', 160, 30, 60, 2], ['B5', 'S1', 'L3', 160, 40, 40, 7]], ['S2', 'L4', 150, 10]], 'B3'], ['generated mix 29', [[['B0', None, None, 0, 0, 60, 5], ['B1', 'S1', 'L3', 100, 0, 60, 7], ['B2', 'S2', 'L2', 160, 40, 60, 7], ['B3', 'S1', 'L3', 131, 30, 60, 3], ['B4', 'S1', 'L1', 145, 10, 20, 4], ['B5', 'S2', 'L1', 130, 5, 40, 3], ['B6', 'S2', 'L2', 160, 40, 60, 5]], ['S1', 'L2', 100, 30]], 'B0']], [['generated mix 34', [[['B0', 'S1', 'L3', 100, 40, 20, 7], ['B1', None, None, 0, 0, 60, 1], ['B2', 'S2', 'L3', 100, 0, 20, 9], ['B3', 'S2', 'L2', 160, 0, 40, 6], ['B4', None, None, 0, 0, 20, 5], ['B5', 'S1', 'L1', 130, 0, 60, 9]], ['S1', 'L2', 130, 10]], 'B1'], ['generated mix 20', [[['B0', None, None, 0, 0, 20, 8], ['B1', 'S1', 'L1', 131, 30, 40, 6], ['B2', None, None, 0, 0, 20, 6], ['B3', 'S1', 'L3', 160, 0, 40, 6], ['B4', 'S1', 'L3', 160, 10, 20, 4], ['B5', 'S1', 'L3', 131, 30, 40, 9], ['B6', None, None, 0, 0, 60, 5]], ['S1', 'L4', 130, 20]], 'B6'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['residual label empty', [[['R', 'S9', 'L7', 50, 0, 60, 2], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'R'], ['nothing', [[['X', 'S2', 'L1', 100, 5, 10, 1]], ['S1', 'L1', 100, 20]], 'HOLD'], ['room exact', [[['A', 'S1', 'L1', 100, 20, 40, 9], ['E', None, None, 0, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['generated mix 32', [[['B0', 'S1', 'L3', 145, 40, 40, 7], ['B1', None, None, 0, 0, 40, 3]], ['S2', 'L4', 150, 20]], 'B1'], ['generated mix 41', [[['B0', 'S1', 'L2', 160, 30, 60, 7], ['B1', 'S1', 'L2', 160, 40, 60, 3], ['B2', 'S1', 'L3', 100, 0, 60, 7], ['B3', 'S2', 'L2', 130, 10, 60, 9], ['B4', 'S1', 'L2', 160, 0, 20, 4], ['B5', 'S2', 'L2', 100, 30, 40, 4]], ['S2', 'L1', 130, 30]], 'B3']], [['generated mix 14', [[['B0', 'S2', 'L1', 130, 5, 60, 3], ['B1', 'S2', 'L1', 120, 0, 60, 8], ['B2', None, None, 0, 0, 60, 9]], ['S1', 'L1', 100, 30]], 'B1'], ['generated mix 67', [[['B0', 'S1', 'L2', 145, 0, 20, 8], ['B1', None, None, 0, 0, 40, 5], ['B2', 'S1', 'L2', 160, 40, 20, 4], ['B3', 'S2', 'L3', 160, 5, 60, 4], ['B4', None, None, 0, 0, 20, 8], ['B5', 'S1', 'L3', 145, 30, 40, 1], ['B6', 'S2', 'L2', 131, 5, 20, 6]], ['S1', 'L4', 130, 20]], 'B1'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['same lot preferred', [[['A', 'S1', 'L1', 100, 10, 40, 9], ['B', 'S1', 'L2', 100, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['residual label empty', [[['R', 'S9', 'L7', 50, 0, 60, 2], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'R'], ['generated mix 43', [[['B0', 'S1', 'L1', 120, 30, 40, 4], ['B1', None, None, 0, 0, 20, 9]], ['S1', 'L4', 130, 10]], 'B0'], ['generated mix 51', [[['B0', 'S2', 'L3', 130, 40, 60, 6], ['B1', 'S1', 'L1', 130, 40, 40, 1], ['B2', 'S1', 'L1', 100, 30, 40, 8], ['B3', 'S1', 'L1', 145, 0, 20, 2]], ['S2', 'L1', 100, 30]], 'HOLD']]]
for label, args, expected in fixtures[N-1]:
check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| generated mix 5 | B4 | B3 | Failed |
| generated mix 3 | B1 | B1 | Passed |
| same lot preferred | A | A | Passed |
| expiry 30 days later | A | A | Passed |
| residual label empty | R | R | Passed |
| nothing | HOLD | HOLD | Passed |
| room exact | A | A | Passed |
| generated mix 8 | B4 | B2 | Failed |
SHA-256 / 516357c9f72156763dccd9d299650a8fc6926994e868b387b4fd44a312e43c92
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(bins, receipt):
sku, lot, exp, q = receipt
def room(b):
return b[5] - b[4] >= q
def empty(b):
return b[1] is None or b[4] == 0
same_lot = [b for b in bins if not empty(b) and b[1] == sku and b[2] == lot and room(b)]
if same_lot:
return min(same_lot, key=lambda b: (b[6], b[0]))[0]
mixed = [b for b in bins if not empty(b) and b[1] == sku and b[2] != lot and abs(b[3] - exp) <= 30 and room(b)]
if mixed:
return min(mixed, key=lambda b: (abs(b[3] - exp), b[6], b[0]))[0]
free = [b for b in bins if empty(b) and b[5] >= q]
if free:
return min(free, key=lambda b: b[0])[0]
return 'HOLD'
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['generated mix 5', [[['B0', 'S1', 'L2', 131, 5, 20, 3], ['B1', None, None, 0, 0, 20, 3], ['B2', None, None, 0, 0, 40, 3], ['B3', None, None, 0, 0, 40, 1], ['B4', None, None, 0, 0, 40, 8], ['B5', None, None, 0, 0, 20, 3], ['B6', 'S2', 'L3', 100, 10, 40, 7]], ['S2', 'L2', 150, 20]], 'B3'], ['generated mix 3', [[['B0', None, None, 0, 0, 60, 5], ['B1', None, None, 0, 0, 60, 4], ['B2', 'S1', 'L1', 145, 30, 60, 2], ['B3', 'S2', 'L2', 100, 40, 20, 1], ['B4', 'S2', 'L1', 131, 30, 40, 2], ['B5', 'S1', 'L1', 145, 30, 40, 7]], ['S1', 'L4', 100, 10]], 'B1'], ['same lot preferred', [[['A', 'S1', 'L1', 100, 10, 40, 9], ['B', 'S1', 'L2', 100, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['residual label empty', [[['R', 'S9', 'L7', 50, 0, 60, 2], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'R'], ['nothing', [[['X', 'S2', 'L1', 100, 5, 10, 1]], ['S1', 'L1', 100, 20]], 'HOLD'], ['room exact', [[['A', 'S1', 'L1', 100, 20, 40, 9], ['E', None, None, 0, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['generated mix 8', [[['B0', 'S1', 'L2', 100, 30, 20, 6], ['B1', 'S2', 'L3', 131, 40, 40, 8], ['B2', None, None, 0, 0, 20, 4], ['B3', 'S2', 'L1', 145, 30, 40, 1], ['B4', 'S2', 'L3', 130, 0, 60, 6]], ['S1', 'L1', 130, 20]], 'B2']], [['generated mix 31', [[['B0', 'S1', 'L1', 145, 5, 60, 4], ['B1', None, None, 0, 0, 60, 7], ['B2', 'S1', 'L3', 160, 10, 60, 7], ['B3', 'S1', 'L1', 160, 0, 60, 8]], ['S1', 'L2', 100, 10]], 'B1'], ['generated mix 24', [[['B0', 'S1', 'L1', 131, 10, 40, 3], ['B1', 'S1', 'L2', 131, 5, 40, 9], ['B2', None, None, 0, 0, 60, 9], ['B3', 'S2', 'L1', 120, 40, 40, 8], ['B4', None, None, 0, 0, 60, 8], ['B5', 'S1', 'L3', 160, 0, 40, 6]], ['S2', 'L1', 130, 30]], 'B5'], ['expiry 31 days', [[['A', 'S1', 'L2', 131, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'E'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['same lot preferred', [[['A', 'S1', 'L1', 100, 10, 40, 9], ['B', 'S1', 'L2', 100, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['generated mix 9', [[['B0', 'S1', 'L2', 160, 40, 40, 1], ['B1', 'S1', 'L1', 131, 40, 40, 1], ['B2', 'S1', 'L3', 145, 0, 60, 1], ['B3', 'S1', 'L2', 100, 10, 20, 7], ['B4', 'S2', 'L1', 100, 30, 60, 5]], ['S1', 'L4', 150, 20]], 'B2'], ['generated mix 17', [[['B0', 'S2', 'L2', 120, 0, 20, 8], ['B1', 'S1', 'L2', 131, 5, 40, 3], ['B2', 'S2', 'L2', 100, 10, 20, 8], ['B3', 'S2', 'L2', 100, 10, 60, 6], ['B4', 'S1', 'L3', 100, 30, 40, 3], ['B5', 'S1', 'L1', 145, 10, 60, 5]], ['S1', 'L4', 150, 20]], 'B5']], [['generated mix 8', [[['B0', 'S1', 'L2', 100, 30, 20, 6], ['B1', 'S2', 'L3', 131, 40, 40, 8], ['B2', None, None, 0, 0, 20, 4], ['B3', 'S2', 'L1', 145, 30, 40, 1], ['B4', 'S2', 'L3', 130, 0, 60, 6]], ['S1', 'L1', 130, 20]], 'B2'], ['generated mix 71', [[['B0', 'S1', 'L3', 160, 5, 40, 5], ['B1', 'S1', 'L1', 130, 0, 60, 7], ['B2', 'S1', 'L3', 120, 0, 60, 2]], ['S2', 'L1', 100, 10]], 'B2'], ['nothing', [[['X', 'S2', 'L1', 100, 5, 10, 1]], ['S1', 'L1', 100, 20]], 'HOLD'], ['room exact', [[['A', 'S1', 'L1', 100, 20, 40, 9], ['E', None, None, 0, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 31 days', [[['A', 'S1', 'L2', 131, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'E'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['generated mix 21', [[['B0', 'S1', 'L3', 130, 40, 60, 3], ['B1', 'S1', 'L3', 145, 30, 40, 7], ['B2', 'S1', 'L2', 130, 40, 60, 4], ['B3', 'S2', 'L2', 120, 30, 40, 3], ['B4', 'S1', 'L3', 160, 30, 60, 2], ['B5', 'S1', 'L3', 160, 40, 40, 7]], ['S2', 'L4', 150, 10]], 'B3'], ['generated mix 29', [[['B0', None, None, 0, 0, 60, 5], ['B1', 'S1', 'L3', 100, 0, 60, 7], ['B2', 'S2', 'L2', 160, 40, 60, 7], ['B3', 'S1', 'L3', 131, 30, 60, 3], ['B4', 'S1', 'L1', 145, 10, 20, 4], ['B5', 'S2', 'L1', 130, 5, 40, 3], ['B6', 'S2', 'L2', 160, 40, 60, 5]], ['S1', 'L2', 100, 30]], 'B0']], [['generated mix 34', [[['B0', 'S1', 'L3', 100, 40, 20, 7], ['B1', None, None, 0, 0, 60, 1], ['B2', 'S2', 'L3', 100, 0, 20, 9], ['B3', 'S2', 'L2', 160, 0, 40, 6], ['B4', None, None, 0, 0, 20, 5], ['B5', 'S1', 'L1', 130, 0, 60, 9]], ['S1', 'L2', 130, 10]], 'B1'], ['generated mix 20', [[['B0', None, None, 0, 0, 20, 8], ['B1', 'S1', 'L1', 131, 30, 40, 6], ['B2', None, None, 0, 0, 20, 6], ['B3', 'S1', 'L3', 160, 0, 40, 6], ['B4', 'S1', 'L3', 160, 10, 20, 4], ['B5', 'S1', 'L3', 131, 30, 40, 9], ['B6', None, None, 0, 0, 60, 5]], ['S1', 'L4', 130, 20]], 'B6'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['residual label empty', [[['R', 'S9', 'L7', 50, 0, 60, 2], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'R'], ['nothing', [[['X', 'S2', 'L1', 100, 5, 10, 1]], ['S1', 'L1', 100, 20]], 'HOLD'], ['room exact', [[['A', 'S1', 'L1', 100, 20, 40, 9], ['E', None, None, 0, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['generated mix 32', [[['B0', 'S1', 'L3', 145, 40, 40, 7], ['B1', None, None, 0, 0, 40, 3]], ['S2', 'L4', 150, 20]], 'B1'], ['generated mix 41', [[['B0', 'S1', 'L2', 160, 30, 60, 7], ['B1', 'S1', 'L2', 160, 40, 60, 3], ['B2', 'S1', 'L3', 100, 0, 60, 7], ['B3', 'S2', 'L2', 130, 10, 60, 9], ['B4', 'S1', 'L2', 160, 0, 20, 4], ['B5', 'S2', 'L2', 100, 30, 40, 4]], ['S2', 'L1', 130, 30]], 'B3']], [['generated mix 14', [[['B0', 'S2', 'L1', 130, 5, 60, 3], ['B1', 'S2', 'L1', 120, 0, 60, 8], ['B2', None, None, 0, 0, 60, 9]], ['S1', 'L1', 100, 30]], 'B1'], ['generated mix 67', [[['B0', 'S1', 'L2', 145, 0, 20, 8], ['B1', None, None, 0, 0, 40, 5], ['B2', 'S1', 'L2', 160, 40, 20, 4], ['B3', 'S2', 'L3', 160, 5, 60, 4], ['B4', None, None, 0, 0, 20, 8], ['B5', 'S1', 'L3', 145, 30, 40, 1], ['B6', 'S2', 'L2', 131, 5, 20, 6]], ['S1', 'L4', 130, 20]], 'B1'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['same lot preferred', [[['A', 'S1', 'L1', 100, 10, 40, 9], ['B', 'S1', 'L2', 100, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['residual label empty', [[['R', 'S9', 'L7', 50, 0, 60, 2], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'R'], ['generated mix 43', [[['B0', 'S1', 'L1', 120, 30, 40, 4], ['B1', None, None, 0, 0, 20, 9]], ['S1', 'L4', 130, 10]], 'B0'], ['generated mix 51', [[['B0', 'S2', 'L3', 130, 40, 60, 6], ['B1', 'S1', 'L1', 130, 40, 40, 1], ['B2', 'S1', 'L1', 100, 30, 40, 8], ['B3', 'S1', 'L1', 145, 0, 20, 2]], ['S2', 'L1', 100, 30]], 'HOLD']]]
for label, args, expected in fixtures[N-1]:
check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| generated mix 5 | B1 | B3 | Failed |
| generated mix 3 | B0 | B1 | Failed |
| same lot preferred | A | A | Passed |
| expiry 30 days later | A | A | Passed |
| residual label empty | E | R | Failed |
| nothing | HOLD | HOLD | Passed |
| room exact | A | A | Passed |
| generated mix 8 | B2 | B2 | Passed |
SHA-256 / cd86faac03c5ec58f74e15b7a8ba05fd6654e56fafa8eefbc53c17731d8a97c7
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(bins, receipt):
sku, lot, exp, q = receipt
def room(b):
return b[5] - b[4] >= q
def empty(b):
return b[1] is None or b[4] == 0
same_lot = [b for b in bins if not empty(b) and b[1] == sku and b[2] == lot and room(b)]
if same_lot:
return min(same_lot, key=lambda b: (b[6], b[0]))[0]
mixed = [b for b in bins if not empty(b) and b[1] == sku and b[2] != lot and abs(b[3] - exp) <= 30 and room(b)]
if mixed:
return min(mixed, key=lambda b: (abs(b[3] - exp), b[6], b[0]))[0]
free = [b for b in bins if empty(b) and b[5] >= q]
if free:
return min(free, key=lambda b: (b[6], b[0]))[0]
return 'HOLD'
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['generated mix 5', [[['B0', 'S1', 'L2', 131, 5, 20, 3], ['B1', None, None, 0, 0, 20, 3], ['B2', None, None, 0, 0, 40, 3], ['B3', None, None, 0, 0, 40, 1], ['B4', None, None, 0, 0, 40, 8], ['B5', None, None, 0, 0, 20, 3], ['B6', 'S2', 'L3', 100, 10, 40, 7]], ['S2', 'L2', 150, 20]], 'B3'], ['generated mix 3', [[['B0', None, None, 0, 0, 60, 5], ['B1', None, None, 0, 0, 60, 4], ['B2', 'S1', 'L1', 145, 30, 60, 2], ['B3', 'S2', 'L2', 100, 40, 20, 1], ['B4', 'S2', 'L1', 131, 30, 40, 2], ['B5', 'S1', 'L1', 145, 30, 40, 7]], ['S1', 'L4', 100, 10]], 'B1'], ['same lot preferred', [[['A', 'S1', 'L1', 100, 10, 40, 9], ['B', 'S1', 'L2', 100, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['residual label empty', [[['R', 'S9', 'L7', 50, 0, 60, 2], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'R'], ['nothing', [[['X', 'S2', 'L1', 100, 5, 10, 1]], ['S1', 'L1', 100, 20]], 'HOLD'], ['room exact', [[['A', 'S1', 'L1', 100, 20, 40, 9], ['E', None, None, 0, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['generated mix 8', [[['B0', 'S1', 'L2', 100, 30, 20, 6], ['B1', 'S2', 'L3', 131, 40, 40, 8], ['B2', None, None, 0, 0, 20, 4], ['B3', 'S2', 'L1', 145, 30, 40, 1], ['B4', 'S2', 'L3', 130, 0, 60, 6]], ['S1', 'L1', 130, 20]], 'B2']], [['generated mix 31', [[['B0', 'S1', 'L1', 145, 5, 60, 4], ['B1', None, None, 0, 0, 60, 7], ['B2', 'S1', 'L3', 160, 10, 60, 7], ['B3', 'S1', 'L1', 160, 0, 60, 8]], ['S1', 'L2', 100, 10]], 'B1'], ['generated mix 24', [[['B0', 'S1', 'L1', 131, 10, 40, 3], ['B1', 'S1', 'L2', 131, 5, 40, 9], ['B2', None, None, 0, 0, 60, 9], ['B3', 'S2', 'L1', 120, 40, 40, 8], ['B4', None, None, 0, 0, 60, 8], ['B5', 'S1', 'L3', 160, 0, 40, 6]], ['S2', 'L1', 130, 30]], 'B5'], ['expiry 31 days', [[['A', 'S1', 'L2', 131, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'E'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['same lot preferred', [[['A', 'S1', 'L1', 100, 10, 40, 9], ['B', 'S1', 'L2', 100, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['generated mix 9', [[['B0', 'S1', 'L2', 160, 40, 40, 1], ['B1', 'S1', 'L1', 131, 40, 40, 1], ['B2', 'S1', 'L3', 145, 0, 60, 1], ['B3', 'S1', 'L2', 100, 10, 20, 7], ['B4', 'S2', 'L1', 100, 30, 60, 5]], ['S1', 'L4', 150, 20]], 'B2'], ['generated mix 17', [[['B0', 'S2', 'L2', 120, 0, 20, 8], ['B1', 'S1', 'L2', 131, 5, 40, 3], ['B2', 'S2', 'L2', 100, 10, 20, 8], ['B3', 'S2', 'L2', 100, 10, 60, 6], ['B4', 'S1', 'L3', 100, 30, 40, 3], ['B5', 'S1', 'L1', 145, 10, 60, 5]], ['S1', 'L4', 150, 20]], 'B5']], [['generated mix 8', [[['B0', 'S1', 'L2', 100, 30, 20, 6], ['B1', 'S2', 'L3', 131, 40, 40, 8], ['B2', None, None, 0, 0, 20, 4], ['B3', 'S2', 'L1', 145, 30, 40, 1], ['B4', 'S2', 'L3', 130, 0, 60, 6]], ['S1', 'L1', 130, 20]], 'B2'], ['generated mix 71', [[['B0', 'S1', 'L3', 160, 5, 40, 5], ['B1', 'S1', 'L1', 130, 0, 60, 7], ['B2', 'S1', 'L3', 120, 0, 60, 2]], ['S2', 'L1', 100, 10]], 'B2'], ['nothing', [[['X', 'S2', 'L1', 100, 5, 10, 1]], ['S1', 'L1', 100, 20]], 'HOLD'], ['room exact', [[['A', 'S1', 'L1', 100, 20, 40, 9], ['E', None, None, 0, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 31 days', [[['A', 'S1', 'L2', 131, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'E'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['generated mix 21', [[['B0', 'S1', 'L3', 130, 40, 60, 3], ['B1', 'S1', 'L3', 145, 30, 40, 7], ['B2', 'S1', 'L2', 130, 40, 60, 4], ['B3', 'S2', 'L2', 120, 30, 40, 3], ['B4', 'S1', 'L3', 160, 30, 60, 2], ['B5', 'S1', 'L3', 160, 40, 40, 7]], ['S2', 'L4', 150, 10]], 'B3'], ['generated mix 29', [[['B0', None, None, 0, 0, 60, 5], ['B1', 'S1', 'L3', 100, 0, 60, 7], ['B2', 'S2', 'L2', 160, 40, 60, 7], ['B3', 'S1', 'L3', 131, 30, 60, 3], ['B4', 'S1', 'L1', 145, 10, 20, 4], ['B5', 'S2', 'L1', 130, 5, 40, 3], ['B6', 'S2', 'L2', 160, 40, 60, 5]], ['S1', 'L2', 100, 30]], 'B0']], [['generated mix 34', [[['B0', 'S1', 'L3', 100, 40, 20, 7], ['B1', None, None, 0, 0, 60, 1], ['B2', 'S2', 'L3', 100, 0, 20, 9], ['B3', 'S2', 'L2', 160, 0, 40, 6], ['B4', None, None, 0, 0, 20, 5], ['B5', 'S1', 'L1', 130, 0, 60, 9]], ['S1', 'L2', 130, 10]], 'B1'], ['generated mix 20', [[['B0', None, None, 0, 0, 20, 8], ['B1', 'S1', 'L1', 131, 30, 40, 6], ['B2', None, None, 0, 0, 20, 6], ['B3', 'S1', 'L3', 160, 0, 40, 6], ['B4', 'S1', 'L3', 160, 10, 20, 4], ['B5', 'S1', 'L3', 131, 30, 40, 9], ['B6', None, None, 0, 0, 60, 5]], ['S1', 'L4', 130, 20]], 'B6'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['residual label empty', [[['R', 'S9', 'L7', 50, 0, 60, 2], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'R'], ['nothing', [[['X', 'S2', 'L1', 100, 5, 10, 1]], ['S1', 'L1', 100, 20]], 'HOLD'], ['room exact', [[['A', 'S1', 'L1', 100, 20, 40, 9], ['E', None, None, 0, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['generated mix 32', [[['B0', 'S1', 'L3', 145, 40, 40, 7], ['B1', None, None, 0, 0, 40, 3]], ['S2', 'L4', 150, 20]], 'B1'], ['generated mix 41', [[['B0', 'S1', 'L2', 160, 30, 60, 7], ['B1', 'S1', 'L2', 160, 40, 60, 3], ['B2', 'S1', 'L3', 100, 0, 60, 7], ['B3', 'S2', 'L2', 130, 10, 60, 9], ['B4', 'S1', 'L2', 160, 0, 20, 4], ['B5', 'S2', 'L2', 100, 30, 40, 4]], ['S2', 'L1', 130, 30]], 'B3']], [['generated mix 14', [[['B0', 'S2', 'L1', 130, 5, 60, 3], ['B1', 'S2', 'L1', 120, 0, 60, 8], ['B2', None, None, 0, 0, 60, 9]], ['S1', 'L1', 100, 30]], 'B1'], ['generated mix 67', [[['B0', 'S1', 'L2', 145, 0, 20, 8], ['B1', None, None, 0, 0, 40, 5], ['B2', 'S1', 'L2', 160, 40, 20, 4], ['B3', 'S2', 'L3', 160, 5, 60, 4], ['B4', None, None, 0, 0, 20, 8], ['B5', 'S1', 'L3', 145, 30, 40, 1], ['B6', 'S2', 'L2', 131, 5, 20, 6]], ['S1', 'L4', 130, 20]], 'B1'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['same lot preferred', [[['A', 'S1', 'L1', 100, 10, 40, 9], ['B', 'S1', 'L2', 100, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['residual label empty', [[['R', 'S9', 'L7', 50, 0, 60, 2], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'R'], ['generated mix 43', [[['B0', 'S1', 'L1', 120, 30, 40, 4], ['B1', None, None, 0, 0, 20, 9]], ['S1', 'L4', 130, 10]], 'B0'], ['generated mix 51', [[['B0', 'S2', 'L3', 130, 40, 60, 6], ['B1', 'S1', 'L1', 130, 40, 40, 1], ['B2', 'S1', 'L1', 100, 30, 40, 8], ['B3', 'S1', 'L1', 145, 0, 20, 2]], ['S2', 'L1', 100, 30]], 'HOLD']]]
for label, args, expected in fixtures[N-1]:
check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| generated mix 5 | B3 | B3 | Passed |
| generated mix 3 | B1 | B1 | Passed |
| same lot preferred | A | A | Passed |
| expiry 30 days later | A | A | Passed |
| residual label empty | R | R | Passed |
| nothing | HOLD | HOLD | Passed |
| room exact | A | A | Passed |
| generated mix 8 | B2 | B2 | Passed |
SHA-256 / 2993bb1690086c45de12be7dd0bc873767fa8c0b0981a8676a7a67043f506029
Verification & scope
Stipulated day-number expiries and a single receipt; no quarantine or multi-bin splits. This reproducer isolates one failure mechanism. Results cover the supplied fixtures. Variants within a family share a test contract and should remain grouped when constructing evaluation splits. Related mechanisms with a shared evaluation_group must also remain together; these controlled models are not independent production incidents.
Observations recorded using Python 3.12.14 at 2026-09-29T14:52:10.142399+00:00.
Case digest / 8dfc5128cd8121761271257631d6366dbbe459815f4a6daaa1851716c9ece225