ProCreations's picture
Reproduction logbook (paper-b3EvCd8sYE)
459fa23 verified
Raw
History Blame Contribute Delete
7.52 kB
"""Claim 5 -- Section 3: computing incomp(G) exactly is NP-hard, because
"computing incomp(G) for an acyclic directed graph after adding all possible
bidirected edges is equivalent to solving the ACYCLIC TRANSITIVITY EDITING
problem" (hardness of the latter: Weller et al., 2012).
What is reproducible here is the REDUCTION, i.e. the identity
incomp( D + all bidirected edges ) == ATE(D) for every acyclic D,
together with the fact that the map D -> D + all bidirected edges is computable
in O(n^2). The external hardness premise (Weller et al. 2012) is ASSUMED, not
reproduced -- see Limitations.
"""
import json, itertools, pickle, random, collections
import gcore
R = {}
def all_digraphs(n):
enc = gcore.Enc(n)
return enc, range(1 << enc.nd)
def ate_table(n):
"""Exact ACYCLIC TRANSITIVITY EDITING optimum for every digraph on n vertices,
by multi-source BFS from all acyclic + transitively closed digraphs."""
enc = gcore.Enc(n)
N = 1 << enc.nd
dist = bytearray([255]) * N
frontier = []
for d in range(N):
succ = gcore.adj_from_dbits(n, enc.dir_pairs, d)
if gcore.is_acyclic(n, succ) and gcore.is_transitively_closed(n, succ):
dist[d] = 0
frontier.append(d)
k = 0
while frontier:
nxt = []
for g in frontier:
for b in range(enc.nd):
h = g ^ (1 << b)
if dist[h] == 255:
dist[h] = k + 1
nxt.append(h)
frontier = nxt
k += 1
return enc, dist, frontier
# ------------------------------------------------- exhaustive reduction check
red = {}
for n in (3, 4):
enc = gcore.Enc(n)
ate_enc, ate, _ = ate_table(n)
dist, compat = gcore.exact_incomp_table(enc)
allbid = 0
for e in enc.bid_pairs:
allbid |= 1 << enc.BID[e]
dags = []
mism = 0
hist_i, hist_a = collections.Counter(), collections.Counter()
for d in range(1 << enc.nd):
succ = gcore.adj_from_dbits(n, enc.dir_pairs, d)
if not gcore.is_acyclic(n, succ):
continue
dags.append(d)
i_val = dist[d | allbid]
a_val = ate[d]
hist_i[i_val] += 1; hist_a[a_val] += 1
if i_val != a_val:
mism += 1
red[n] = dict(n_dags=len(dags), mismatches=mism,
incomp_histogram=dict(sorted(hist_i.items())),
ate_histogram=dict(sorted(hist_a.items())),
max_value=max(hist_i))
print("n=%d" % n, red[n], flush=True)
R["reduction_exhaustive"] = red
# ------------------------------------------------------------ negative controls
ctl = {}
# (a) robustness probe only: the paper's reduction is stated for acyclic input.
# Do not assume that the finite identity must fail outside that scope; record the
# result literally and do not use it as a destructive control.
for n in (3, 4):
enc = gcore.Enc(n)
_, ate, _ = ate_table(n)
dist, _ = gcore.exact_incomp_table(enc)
allbid = 0
for e in enc.bid_pairs:
allbid |= 1 << enc.BID[e]
tot = mism = 0
for d in range(1 << enc.nd):
succ = gcore.adj_from_dbits(n, enc.dir_pairs, d)
if gcore.is_acyclic(n, succ):
continue
tot += 1
if dist[d | allbid] != ate[d]:
mism += 1
ctl[f"cyclic inputs n={n}"] = dict(cyclic_digraphs=tot, mismatches=mism,
mismatch_rate=mism / tot if tot else 0)
print("control (cyclic inputs) n=%d" % n, ctl[f"cyclic inputs n={n}"], flush=True)
# (b) drop the "add all bidirected edges" step of the reduction
for n in (3, 4):
enc = gcore.Enc(n)
_, ate, _ = ate_table(n)
dist, _ = gcore.exact_incomp_table(enc)
tot = mism = 0
for d in range(1 << enc.nd):
succ = gcore.adj_from_dbits(n, enc.dir_pairs, d)
if not gcore.is_acyclic(n, succ):
continue
tot += 1
if dist[d] != ate[d]: # no bidirected edges added
mism += 1
ctl[f"no bidirected edges added n={n}"] = dict(dags=tot, mismatches=mism,
mismatch_rate=mism / tot)
print("control (skip 'add all bidirected') n=%d" % n,
ctl[f"no bidirected edges added n={n}"], flush=True)
# (c) drop transitive closure from the target predicate (acyclicity only)
for n in (4,):
enc = gcore.Enc(n)
N = 1 << enc.nd
dist_ac = bytearray([255]) * N
fr = []
for d in range(N):
if gcore.is_acyclic(n, gcore.adj_from_dbits(n, enc.dir_pairs, d)):
dist_ac[d] = 0; fr.append(d)
k = 0
while fr:
nxt = []
for g in fr:
for b in range(enc.nd):
h = g ^ (1 << b)
if dist_ac[h] == 255:
dist_ac[h] = k + 1; nxt.append(h)
fr = nxt; k += 1
_, ate, _ = ate_table(n)
tot = mism = 0
for d in range(N):
if not gcore.is_acyclic(n, gcore.adj_from_dbits(n, enc.dir_pairs, d)):
continue
tot += 1
if dist_ac[d] != ate[d]:
mism += 1
ctl[f"acyclicity-only target n={n}"] = dict(dags=tot, mismatches=mism,
mismatch_rate=mism / tot)
print("control (acyclicity-only target) n=4", ctl["acyclicity-only target n=4"], flush=True)
R["controls"] = ctl
# --------------------------------------------------------- sampled n = 5 check
enc5 = gcore.Enc(5)
rng = random.Random(5)
allbid5 = 0
for e in enc5.bid_pairs:
allbid5 |= 1 << enc5.BID[e]
def ate_bounded(n, d, ub=6):
enc = gcore.Enc(n)
succ = gcore.adj_from_dbits(n, enc.dir_pairs, d)
if gcore.is_acyclic(n, succ) and gcore.is_transitively_closed(n, succ):
return 0
for k in range(1, ub + 1):
for combo in itertools.combinations(range(enc.nd), k):
h = d
for b in combo:
h ^= 1 << b
s2 = gcore.adj_from_dbits(n, enc.dir_pairs, h)
if gcore.is_acyclic(n, s2) and gcore.is_transitively_closed(n, s2):
return k
return None
n5, mism5, vals5 = 0, 0, []
tries = 0
while n5 < 120 and tries < 6000:
tries += 1
d = rng.getrandbits(enc5.nd)
succ = gcore.adj_from_dbits(5, enc5.dir_pairs, d)
if not gcore.is_acyclic(5, succ):
continue
a = ate_bounded(5, d, 4)
if a is None:
continue
i = gcore.bounded_incomp(enc5, d | allbid5, 4)
n5 += 1
vals5.append(a)
if a != i:
mism5 += 1
R["sampled_n5"] = dict(dags_tested=n5, mismatches=mism5,
ate_histogram=dict(sorted(collections.Counter(vals5).items())))
print("sampled n=5:", R["sampled_n5"], flush=True)
# ----------------------------------------- reduction is polynomial-time (O(n^2))
R["reduction_cost"] = {str(n): dict(bidirected_edges_added=n * (n - 1) // 2,
total_edges=n * (n - 1) + n * (n - 1) // 2)
for n in (3, 4, 5, 10, 50)}
# ------------- the paper's remark: deciding incomp(G) <= k is polynomial for fixed k
R["ball_search_sizes"] = {}
for n in (5, 10, 20, 50):
bits = n * (n - 1) + n * (n - 1) // 2
R["ball_search_sizes"][str(n)] = {f"k={k}": int(
__import__("math").comb(bits, k)) for k in (1, 2, 3)}
print("ball sizes (poly in n for fixed k):", R["ball_search_sizes"]["10"], flush=True)
json.dump(R, open("outputs/claim5.json", "w"), indent=1, default=str)
print("\nwrote outputs/claim5.json")