#!/usr/bin/env python3 """Independent enumeration of paired multiplicities and search-log coverage. Unlike the producer, this checker does not partition kappa and r separately or align their partitions. It recursively generates sorted pairs (kappa_u,r_u). Only Python's standard library is used. """ from __future__ import annotations from collections import Counter from pathlib import Path import argparse import json import re PAIRS = [(8,5),(9,5),(10,5),(10,6),(11,5),(11,6),(11,7)] def joint_rows(length: int, sum_k: int, sum_r: int, low: tuple[int,int]=(1,1)): if length == 1: if sum_k > 0 and sum_r > 0 and (sum_k,sum_r) >= low: yield ((sum_k,sum_r),) return for a in range(low[0], sum_k//length + 1): for b in range(low[1] if a == low[0] else 1, sum_r-length+2): for tail in joint_rows(length-1,sum_k-a,sum_r-b,(a,b)): yield ((a,b),)+tail def expected_cases(k: int, h: int) -> set[tuple]: out=set() for t in range(k+1, min(3*h,3*(k-h))+1): for row_data in joint_rows(t,3*(k-h),3*h): counts=Counter(row_data) for p1 in range(min(1,counts[(1,2)])+1): for p2 in range(min(1-p1,counts[(2,1)])+1): for d in range(min(k-p1-p2,counts[(1,1)])+1): if 2*d+p1+p2 > t: continue remain=counts.copy() remain[(1,1)]-=d remain[(1,2)]-=p1 remain[(2,1)]-=p2 out.add((t,d,p1,p2,tuple(sorted(remain.elements())))) return out def read_cases(path: Path) -> dict[int,tuple]: rows={} for line in path.read_text().splitlines(): x=list(map(int,line.split())) i,t,d,p1,p2=x[:5] assert len(x)==5+2*(t-d-p1-p2) assert i not in rows rows[i]=(t,d,p1,p2,tuple(zip(x[5::2],x[6::2]))) assert set(rows)==set(range(len(rows))) assert len(set(rows.values()))==len(rows) return rows def read_log(path: Path, cases: dict[int,tuple]) -> tuple[set[int],dict]: lines=path.read_text().splitlines() assert lines[-1].startswith('COMPLETE '), f'Incomplete search: {path}' end={a:int(b) for a,b in re.findall(r'(\w+)=(\d+)',lines[-1])} assert end['found']==0 ids=set() for line in lines[:-1]: assert line.startswith('case='), f'Unexpected log line: {line}' d={a:int(b) for a,b in re.findall(r'(\w+)=(\d+)',line)} i=d['case']; assert i in cases and i not in ids assert (d['t'],d['d'],d['p1'],d['p2'])==cases[i][:4] ids.add(i) assert len(ids)==end['cases'] return ids,end def main(): if not __debug__: raise RuntimeError('Run without -O: assertion checks are required.') ap=argparse.ArgumentParser(description=__doc__) ap.add_argument('results',type=Path) ap.add_argument('--output',type=Path) args=ap.parse_args() reports=[] for k,h in PAIRS: root=args.results cases=read_cases(root/f'k{k}_h{h}_cases.txt') expected=expected_cases(k,h) assert set(cases.values())==expected, f'Incomplete or extra root cases for {(k,h)}' logs=([root/f'k11_h6_{i}.txt' for i in range(4)] if (k,h)==(11,6) else [root/f'k{k}_h{h}.txt']) seen=set(); totals=Counter() for j,path in enumerate(logs): ids,end=read_log(path,cases) assert not (seen & ids) if len(logs)>1: assert all(i % len(logs)==j for i in ids) seen |= ids; totals.update(end) assert seen == set(cases), f'Missing search root for {(k,h)}' reports.append({'k':k,'h':h,'root_cases':len(cases), 'independent_row_generation':True, 'search_coverage_complete':True,'totals':dict(totals)}) text=json.dumps({'all_coverage_verified':True,'parameters':reports},indent=2)+'\n' if args.output: args.output.write_text(text) print(text,end='') if __name__=='__main__': main()