complexity.py
8.9 kB · python · 301 lines
1import itertools2from itertools import product3from fractions import Fraction4from .bang import anf_coefficients56# COMPLEXITY MEASURES - THE STANDARD BOOLEAN-FUNCTION TOOLKIT, FROM SCRATCH78def _value(filled):9 f = filled10 def ev(x):11 return 1 if x in f else 012 return ev1314def _flip(corner, axis):15 return tuple(corner[j] ^ (1 if j == axis else 0) for j in range(len(corner)))1617# SENSITIVITY1819def sensitivity_at(filled, cells, x):20 fx = 1 if x in filled else 021 count = 022 for axis in range(len(x)):23 y = _flip(x, axis)24 fy = 1 if y in filled else 025 if fy != fx:26 count += 127 return count2829def sensitivity(filled, cells):30 return max((sensitivity_at(filled, cells, x) for x in cells), default=0)3132# BLOCK SENSITIVITY3334def _flip_block(corner, block):35 s = set(block)36 return tuple(corner[j] ^ (1 if j in s else 0) for j in range(len(corner)))3738def _sensitive_blocks(filled, cells, x):39 D = len(x)40 fx = 1 if x in filled else 041 blocks = []42 for r in range(1, D + 1):43 for block in itertools.combinations(range(D), r):44 y = _flip_block(x, block)45 fy = 1 if y in filled else 046 if fy != fx:47 blocks.append(frozenset(block))48 return blocks4950def _minimal_blocks(blocks):51 out = []52 for b in blocks:53 if not any(other < b for other in blocks):54 out.append(b)55 return out5657def _max_disjoint(blocks):58 blocks = sorted(set(blocks), key=lambda b: (len(b), tuple(sorted(b))))59 best = 060 def search(i, used, count):61 nonlocal best62 if count + (len(blocks) - i) <= best:63 return64 if i == len(blocks):65 best = max(best, count)66 return67 b = blocks[i]68 if used.isdisjoint(b):69 search(i + 1, used | b, count + 1)70 search(i + 1, used, count)71 search(0, frozenset(), 0)72 return best7374def block_sensitivity_at(filled, cells, x):75 blocks = _minimal_blocks(_sensitive_blocks(filled, cells, x))76 return _max_disjoint(blocks)7778def block_sensitivity(filled, cells):79 return max((block_sensitivity_at(filled, cells, x) for x in cells), default=0)8081# CERTIFICATE COMPLEXITY8283def _is_certificate(filled, cells, x, S):84 fx = 1 if x in filled else 085 Sset = set(S)86 for y in cells:87 if all(y[j] == x[j] for j in Sset):88 fy = 1 if y in filled else 089 if fy != fx:90 return False91 return True9293def certificate_at(filled, cells, x):94 D = len(x)95 for r in range(D + 1):96 for S in itertools.combinations(range(D), r):97 if _is_certificate(filled, cells, x, S):98 return r99 return D100101def certificate_complexity(filled, cells):102 return max((certificate_at(filled, cells, x) for x in cells), default=0)103104def certificate_complexity_1(filled, cells):105 vals = [certificate_at(filled, cells, x) for x in cells if x in filled]106 return max(vals, default=0)107108def certificate_complexity_0(filled, cells):109 vals = [certificate_at(filled, cells, x) for x in cells if x not in filled]110 return max(vals, default=0)111112# DECISION-TREE DEPTH113114def _restrict(filled_sub, axis, value):115 return frozenset(c for c in filled_sub if c[axis] == value)116117def _all_sub(cells_sub, axis, value):118 return [c for c in cells_sub if c[axis] == value]119120def _constant_value(filled_count, total_count):121 if filled_count == 0:122 return 0123 if filled_count == total_count:124 return 1125 return None126127def decision_tree_depth(filled, cells):128 D = len(cells[0]) if cells else 0129 full = frozenset(cells)130 memo = {}131 def depth(sub_cells, free_axes):132 key = (sub_cells, free_axes)133 if key in memo:134 return memo[key]135 lit = sum(1 for c in sub_cells if c in filled)136 if lit == 0 or lit == len(sub_cells):137 memo[key] = 0138 return 0139 best = None140 for axis in free_axes:141 rest = tuple(a for a in free_axes if a != axis)142 c0 = tuple(c for c in sub_cells if c[axis] == 0)143 c1 = tuple(c for c in sub_cells if c[axis] == 1)144 d = 1 + max(depth(c0, rest), depth(c1, rest))145 if best is None or d < best:146 best = d147 memo[key] = best148 return best149 return depth(tuple(cells), tuple(range(D)))150151# REAL (FOURIER) POLYNOMIAL DEGREE152153def real_anf_coefficients(filled, cells):154 D = len(cells[0])155 coeff = {c: (1 if c in filled else 0) for c in cells}156 for axis in range(D):157 for c in cells:158 if c[axis] == 1:159 lower = tuple(c[j] if j != axis else 0 for j in range(D))160 coeff[c] = coeff[c] - coeff[lower]161 return coeff162163def real_degree(filled, cells):164 coeff = real_anf_coefficients(filled, cells)165 degs = [sum(c) for c in cells if coeff[c] != 0]166 return max(degs, default=-1)167168# DNF AND CNF SIZE169170def _pattern_cells(pattern, cells):171 out = []172 for c in cells:173 if all(p == -1 or p == c[j] for j, p in enumerate(pattern)):174 out.append(c)175 return frozenset(out)176177def _prime_implicants(filled, cells):178 D = len(cells[0])179 onset = frozenset(c for c in cells if c in filled)180 current = {tuple(c) for c in onset}181 primes = []182 while current:183 used = set()184 nxt = set()185 for a in current:186 for b in current:187 if a >= b:188 continue189 diff = [j for j in range(D) if a[j] != b[j]]190 if len(diff) == 1:191 j = diff[0]192 if a[j] == -1 or b[j] == -1:193 continue194 merged = tuple(-1 if k == j else a[k] for k in range(D))195 nxt.add(merged)196 used.add(a)197 used.add(b)198 for a in current:199 if a not in used:200 primes.append(a)201 current = nxt202 out = []203 seen = set()204 for pat in primes:205 if pat in seen:206 continue207 seen.add(pat)208 out.append((pat, _pattern_cells(pat, cells)))209 return out, onset210211def _min_cover(onset, prime_covers):212 onset = frozenset(onset)213 if not onset:214 return 0215 covers = [c & onset for c in prime_covers if c & onset]216 best = [len(onset) + 1]217 def search(remaining, used):218 if not remaining:219 best[0] = min(best[0], used)220 return221 if used + 1 >= best[0]:222 return223 target = next(iter(remaining))224 options = [c for c in covers if target in c]225 options.sort(key=lambda c: len(c & remaining), reverse=True)226 for c in options:227 search(remaining - c, used + 1)228 search(onset, 0)229 return best[0]230231def dnf_size(filled, cells):232 primes, onset = _prime_implicants(filled, cells)233 if not onset:234 return 0235 return _min_cover(onset, [cov for _, cov in primes])236237def cnf_size(filled, cells):238 full = frozenset(cells)239 complement = full - frozenset(filled)240 return dnf_size(complement, cells)241242# THE FILL-POLYNOMIAL FINGERPRINT243244def _fill_at_odd_base(filled, cells, k):245 D = len(cells[0])246 E = k247 O = k - 1248 total = 0249 for c in filled:250 pc = sum(c)251 total += (E ** (D - pc)) * (O ** pc)252 return total253254def _fit_polynomial(points):255 n = len(points)256 xs = [Fraction(x) for x, _ in points]257 ys = [Fraction(y) for _, y in points]258 coeffs = [Fraction(0)] * n259 for i in range(n):260 Li = [Fraction(0)] * n261 Li[0] = Fraction(1)262 denom = Fraction(1)263 for j in range(n):264 if j == i:265 continue266 denom *= (xs[i] - xs[j])267 new = [Fraction(0)] * n268 for d in range(n - 1):269 new[d + 1] += Li[d]270 for d in range(n):271 new[d] += -xs[j] * Li[d]272 Li = new273 scale = ys[i] / denom274 for d in range(n):275 coeffs[d] += scale * Li[d]276 return coeffs277278def fill_fingerprint(filled, cells):279 D = len(cells[0])280 points = [(k, _fill_at_odd_base(filled, cells, k)) for k in range(1, D + 2)]281 coeffs = _fit_polynomial(points)282 leading_first = list(reversed(coeffs))283 cleaned = []284 for a in leading_first:285 cleaned.append(int(a) if a.denominator == 1 else a)286 return cleaned287288def measures(filled, cells):289 return {290 "popcount": len(filled),291 "sensitivity": sensitivity(filled, cells),292 "block_sensitivity": block_sensitivity(filled, cells),293 "certificate": certificate_complexity(filled, cells),294 "certificate_1": certificate_complexity_1(filled, cells),295 "certificate_0": certificate_complexity_0(filled, cells),296 "decision_tree_depth": decision_tree_depth(filled, cells),297 "real_degree": real_degree(filled, cells),298 "dnf_size": dnf_size(filled, cells),299 "cnf_size": cnf_size(filled, cells),300 "fill_fingerprint": fill_fingerprint(filled, cells),301 }