"""Zero-forcing closure oracle (definition D3) with complete chronology, replay, and the order-independence test T2 (empirical companion of Proposition P1). A coloring is a set of black vertices. A black vertex b with exactly one white neighbour w may force w. closure() performs forces until none is available and returns (final_black_set, chronology); the chronology is the list of forces (b, w) in the order performed. The scan order used to pick the next force is a parameter (fixed increasing order, or a seeded random permutation at every step); by P1 the final set does not depend on it. """ import json import random import sys import common # noqa: F401 from graph_constructor import adjacency, label, labels_to_ids def closure(adj, S, order="fixed", rng=None): """adj: adjacency lists; S: iterable of vertex ids. order: 'fixed' (lowest-index available force first) or 'random' (a fresh random permutation of the candidate black vertices at every step, using rng).""" N = len(adj) black = [False] * N for w in S: black[w] = True chron = [] while True: candidates = range(N) if order == "fixed" else rng.sample(range(N), N) forced = False for b in candidates: if not black[b]: continue white = [x for x in adj[b] if not black[x]] if len(white) == 1: w = white[0] black[w] = True chron.append((b, w)) forced = True break # restart the scan: 'fixed' order is then deterministic if not forced: break final = {w for w in range(N) if black[w]} return final, chron def closure_fast(adjmask, Smask): """Bitmask closure (same rule); used by solvers for speed. Returns final mask.""" B = Smask N = len(adjmask) changed = True while changed: changed = False rest = B while rest: low = rest & -rest b = low.bit_length() - 1 rest ^= low white = adjmask[b] & ~B if white and (white & (white - 1)) == 0: B |= white changed = True return B def is_zero_forcing(adj, S): final, _ = closure(adj, S) return len(final) == len(adj) def replay(adj, S, chron): """Replays a chronology, checking legality of every force. Returns (ok, final_black_set, first_bad_index).""" N = len(adj) black = [False] * N for w in S: black[w] = True for idx, (b, w) in enumerate(chron): if not black[b] or black[w]: return False, {x for x in range(N) if black[x]}, idx white = [x for x in adj[b] if not black[x]] if white != [w]: return False, {x for x in range(N) if black[x]}, idx black[w] = True return True, {x for x in range(N) if black[x]}, None def order_independence_test(n, k, seeds_per_n=200, orders_per_seed=20, sizes=(3, 8), seed=20261030): """T2: for random S, compare the final black set over random force orders.""" adj = adjacency(n, k) N = 2 * n rng = random.Random(seed * 1000 + n) passed = 0 total = 0 for _ in range(seeds_per_n): s = rng.randint(sizes[0], sizes[1]) S = rng.sample(range(N), s) ref, _ = closure(adj, S) finals = set() for _ in range(orders_per_seed): fin, _ = closure(adj, S, order="random", rng=rng) finals.add(frozenset(fin)) total += 1 if len(finals) == 1 and frozenset(ref) in finals: passed += 1 return passed, total if __name__ == "__main__": k = 5 report = [] allok = True for n in range(11, 17): p, t = order_independence_test(n, k) print("T2 n=%d: %d/%d seed sets had an order-independent closure" % (n, p, t)) report.append({"n": n, "k": k, "passed": p, "total": t}) allok &= p == t print("T2 ALL OK" if allok else "T2 FAILURE") with open("results/t2_order_independence.json", "w") as f: json.dump(report, f, indent=1)