Sweep all diamond insertion sites; report first-match vs full sweep

run_graph no longer takes the first admissible seam edge per odd seam. It now
enumerates every valid diamond site per odd seam (_candidate_sites), sweeps the
full Cartesian product (capped by --max-combos), runs <=4 colour phases per
combination, and counts a graph ok iff SOME placement fully descends. Reports
both the old first-match tally and the swept tally, plus design-space stats and
how many graphs the sweep rescued.

Finding: most "fail:diamond-switch" cases were heuristic, not intrinsic. The
old 39/60 was the first-match heuristic (one point in the design space, and
seed-sensitive 31-39). Sweeping insertion sites rescues ~20 of ~24 first-match
failures:

  seed 1:  first-match 31 ok / 29 fail  ->  sweep 54 ok / 6 fail  (rescued 23)
  seed 2:  first-match 36 ok / 24 fail  ->  sweep 57 ok / 3 fail  (rescued 21)

Only ~3-6 fail:diamond-switch survive the full site sweep -- those are the real
obstruction targets for the joint {1,3}-cycle bipartiteness solver. The colour/
tread phase is still only randomized over 4 attempts, not enumerated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 23:08:17 -04:00
parent c6e2c3e1a5
commit 2ff712b994
2 changed files with 179 additions and 48 deletions
@@ -42,12 +42,31 @@ consecutive around a vertex or face.
## Status (synthetic ring triangulations, the clean-level-structure domain) ## Status (synthetic ring triangulations, the clean-level-structure domain)
Pipeline runs end to end. Surgery, canonical colouring, and gadget removal all Pipeline runs end to end. Surgery, canonical colouring, and gadget removal all
work. The program now lands squarely on the **cycle layer**: work. The program now lands squarely on the **cycle layer**.
The original `60 random ring triangulations: 39 ok, 21 fail` figure was the
**first-match heuristic** — one diamond per odd seam, placed at the *first*
admissible seam edge, only the colouring phase varied (≤4 random tread phases).
That is one point in the insertion-site design space, not a sweep of it.
**Site sweep (`run_graph` now enumerates every combination of insertion sites,
one per odd seam, ≤4 colour phases each; `--max-combos` caps the product).**
A graph counts `ok` iff *some* placement fully descends:
``` ```
60 random ring triangulations: 39 ok, 21 fail:diamond-switch seed 1, 60 graphs: first-match 31 ok / 29 fail -> sweep 54 ok / 6 fail (rescued 23)
seed 2, 60 graphs: first-match 36 ok / 24 fail -> sweep 57 ok / 3 fail (rescued 21)
``` ```
(First-match is seed-sensitive — 3139 depending on seed; the 39 was one such
seed. What is robust is the *gap*: sweeping insertion sites rescues ~20 of the
~24 first-match failures, leaving a small stubborn residue of ~36
`fail:diamond-switch` graphs.) Design space is real but modest: ~50 graphs need
a diamond, ~2900 combos total, max ~9001200 on a single graph (a handful hit
the cap). So the answer to "did we test every way of adding a diamond?" is:
**now yes** (per odd seam, up to the cap), and most of the apparent failures
were heuristic, not intrinsic.
**Crucial diagnostic:** for a failing case, a simultaneously-removable proper **Crucial diagnostic:** for a failing case, a simultaneously-removable proper
3-colouring of `M(G')` was shown to **exist** (it must — `M(G)` is 3-colourable). 3-colouring of `M(G')` was shown to **exist** (it must — `M(G)` is 3-colourable).
So `fail:diamond-switch` is **not** non-existence; it is **Kempe-reachability** So `fail:diamond-switch` is **not** non-existence; it is **Kempe-reachability**
@@ -38,7 +38,7 @@ import os
import subprocess import subprocess
import sys import sys
from collections import defaultdict, deque from collections import defaultdict, deque
from itertools import combinations from itertools import combinations, product
HERE = os.path.dirname(os.path.abspath(__file__)) HERE = os.path.dirname(os.path.abspath(__file__))
PLANTRI = os.path.join(HERE, "..", "..", "..", "plantri", "plantri") PLANTRI = os.path.join(HERE, "..", "..", "..", "plantri", "plantri")
@@ -574,55 +574,127 @@ def verify_proper(g, col):
return True return True
def run_graph(g0: Tri, outer=None, verbose=False, attempts=4): def run_graph(g0: Tri, outer=None, verbose=False, attempts=4, max_combos=128):
"""Run the program on g0, sweeping EVERY combination of diamond insertion
sites (one site per odd seam) rather than the old first-match heuristic.
Returns a dict:
status -- 'ok' if SOME placement (over <=attempts colour phases)
succeeds, else a representative 'skip:'/'fail:' string.
first_status -- result of the first-match heuristic (old behaviour:
first valid seam edge per seam) for comparison.
n_diamond_seams -- number of odd seams needing a diamond.
n_combos -- size of the full insertion-site design space.
n_combos_tried -- combos actually run (<= max_combos; rest truncated).
n_combos_ok -- how many tried combos fully succeeded.
truncated -- True if n_combos > max_combos.
"""
import random import random
g0.check() g0.check()
if outer is None: if outer is None:
outer = tuple(g0.faces()[0]) outer = tuple(g0.faces()[0])
an0 = Analysis(g0.copy(), outer) orig_vertices = set(g0.rot)
if an0.degenerate:
return f"skip:{an0.degenerate}" prep = _prep_gadgets(g0, outer)
if isinstance(prep, str):
return {"status": prep, "first_status": prep, "n_diamond_seams": 0,
"n_combos": 0, "n_combos_tried": 0, "n_combos_ok": 0,
"truncated": False}
template, an_g, gadgets = prep
sites = _candidate_sites(an_g)
if sites is None:
s = "skip:no-diamond-site"
return {"status": s, "first_status": s, "n_diamond_seams": 0,
"n_combos": 0, "n_combos_tried": 0, "n_combos_ok": 0,
"truncated": False}
# product(*choices) yields the first-match combo (all first elements) first.
combos = list(product(*sites)) if sites else [()]
n_combos = len(combos)
truncated = n_combos > max_combos
if truncated:
combos = combos[:max_combos]
def run_combo(combo):
last = "fail:unknown" last = "fail:unknown"
for att in range(attempts): for att in range(attempts):
rng = random.Random(1000 + att) rng = random.Random(1000 + att)
last = _attempt(g0, outer, rng, verbose) last = _descend_with_sites(template, outer, orig_vertices,
gadgets, combo, rng)
if last == "ok" or last.startswith("skip"): if last == "ok" or last.startswith("skip"):
return last return last
return last return last
first_status = run_combo(combos[0])
n_ok = 1 if first_status == "ok" else 0
swept_status = first_status
for combo in combos[1:]:
res = run_combo(combo)
if res == "ok":
n_ok += 1
swept_status = "ok"
elif swept_status != "ok" and not swept_status.startswith("fail"):
# keep a 'fail:' over a 'skip:' as the representative non-ok status
swept_status = res
if swept_status != "ok" and first_status.startswith("fail"):
swept_status = first_status
def _attempt(g0: Tri, outer, rng, verbose=False): return {"status": swept_status, "first_status": first_status,
"n_diamond_seams": len(sites), "n_combos": n_combos,
"n_combos_tried": len(combos), "n_combos_ok": n_ok,
"truncated": truncated}
def _prep_gadgets(g0: Tri, outer):
"""Insert the leaf gadget on every terminal triangle (deterministic), then
re-analyse. Returns (g_with_gadgets, Analysis, gadgets) or a 'skip:' str."""
g = g0.copy() g = g0.copy()
an = Analysis(g, outer) an = Analysis(g, outer)
if an.degenerate: if an.degenerate:
return f"skip:{an.degenerate}" return f"skip:{an.degenerate}"
gadgets = []
# --- surgeries -------------------------------------------------------
diamonds = [] # (w, quad(a,b,c,d) with diagonal (u,v)) for removal
gadgets = [] # (y, z, u, v, x, t)
for f in an.terminal: for f in an.terminal:
a, b, c = f a, b, c = f
y, z, u, v, x, t = g.insert_leaf_gadget(a, b, c) y, z, u, v, x, t = g.insert_leaf_gadget(a, b, c)
gadgets.append((y, z, u, v, x, t)) gadgets.append((y, z, u, v, x, t))
# re-analyse (gadgets change seams)
an = Analysis(g, outer) an = Analysis(g, outer)
if an.degenerate: if an.degenerate:
return f"skip:post-gadget-{an.degenerate}" return f"skip:post-gadget-{an.degenerate}"
return g, an, gadgets
def _candidate_sites(an: Analysis):
"""For each ODD seam (in an.seams order), the list of valid diamond edges
-- seam edges whose two apexes straddle levels k-1 and k+1. Returns a list
(one entry per odd seam) of edge lists, or None if some odd seam has no
valid site (the old 'skip:no-diamond-site')."""
g = an.g
choices = []
for k, cyc in an.seams: for k, cyc in an.seams:
if len(cyc) % 2 == 0: if len(cyc) % 2 == 0:
continue continue
# choose a seam edge whose below-apex is strictly deeper edges = []
choice = None
for i in range(len(cyc)): for i in range(len(cyc)):
a, b = cyc[i], cyc[(i + 1) % len(cyc)] a, b = cyc[i], cyc[(i + 1) % len(cyc)]
x, t = g.apexes_of(a, b) x, t = g.apexes_of(a, b)
lx, lt = an.level[x], an.level[t] lx, lt = an.level[x], an.level[t]
if {lx, lt} == {k - 1, k + 1}: if {lx, lt} == {k - 1, k + 1}:
choice = (a, b) edges.append((a, b))
break if not edges:
if choice is None: return None
return "skip:no-diamond-site" choices.append(edges)
w, u, v, x, t = g.insert_diamond(*choice) return choices
def _descend_with_sites(template: Tri, outer, orig_vertices, gadgets, combo,
rng):
"""One full run with diamonds inserted at the explicit sites in `combo`
(one (a,b) edge per odd seam), then canonical colour + Kempe descent."""
g = template.copy()
diamonds = [] # (w, u, v, x, t) for removal
for (a, b) in combo:
w, u, v, x, t = g.insert_diamond(a, b)
diamonds.append((w, u, v, x, t)) diamonds.append((w, u, v, x, t))
an = Analysis(g, outer) an = Analysis(g, outer)
if an.degenerate: if an.degenerate:
@@ -636,7 +708,6 @@ def _attempt(g0: Tri, outer, rng, verbose=False):
return f"fail:canonical-{reason}" return f"fail:canonical-{reason}"
# --- descent ----------------------------------------------------------- # --- descent -----------------------------------------------------------
level = an.level
# diamonds: remove each (diagonal uv) # diamonds: remove each (diagonal uv)
for w, u, v, x, t in diamonds: for w, u, v, x, t in diamonds:
adj = medial_adj(g) adj = medial_adj(g)
@@ -682,7 +753,7 @@ def _attempt(g0: Tri, outer, rng, verbose=False):
return "fail:gadget-removal" return "fail:gadget-removal"
# --- final check -------------------------------------------------------- # --- final check --------------------------------------------------------
if set(g.rot) != set(g0.rot): if set(g.rot) != orig_vertices:
return "fail:vertex-mismatch" return "fail:vertex-mismatch"
if not verify_proper(g, col): if not verify_proper(g, col):
return "fail:final-improper" return "fail:final-improper"
@@ -792,6 +863,47 @@ def random_profile(rng):
return sizes, leaf return sizes, leaf
def _record(res, swept_tally, first_tally, sweep):
"""Fold one run_graph dict (or error string) into the running tallies.
`sweep` accumulates site-sweep diagnostics."""
if isinstance(res, str): # error before the sweep
swept_tally[res] += 1
first_tally[res] += 1
return res
swept_tally[res["status"]] += 1
first_tally[res["first_status"]] += 1
if res["n_diamond_seams"]:
sweep["graphs_with_diamonds"] += 1
sweep["total_combos"] += res["n_combos"]
sweep["total_combos_tried"] += res["n_combos_tried"]
sweep["max_combos_seen"] = max(sweep["max_combos_seen"],
res["n_combos"])
if res["truncated"]:
sweep["truncated_graphs"] += 1
# the headline: a graph the first-match heuristic FAILED but some
# placement RESCUED.
if res["status"] == "ok" and res["first_status"] != "ok":
sweep["rescued_by_sweep"] += 1
return res["status"]
def _print_sweep(sweep, first_tally, swept_tally):
fline = " ".join(f"{k}={v}" for k, v in sorted(first_tally.items()))
sline = " ".join(f"{k}={v}" for k, v in sorted(swept_tally.items()))
print("\n--- first-match heuristic (old behaviour) ---")
print(" " + fline)
print("--- full site sweep (some-placement-works) ---")
print(" " + sline)
g = sweep["graphs_with_diamonds"]
print(f"\nsite-sweep stats: {g} graphs needed >=1 diamond; "
f"design space {sweep['total_combos']} combos total "
f"(max {sweep['max_combos_seen']} on one graph; "
f"{sweep['truncated_graphs']} graphs truncated at the cap), "
f"{sweep['total_combos_tried']} combos actually run.")
print(f"sweep rescued {sweep['rescued_by_sweep']} graph(s) that the "
f"first-match heuristic failed.")
def main(): def main():
ap = argparse.ArgumentParser(description=__doc__) ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--min-n", type=int, default=6) ap.add_argument("--min-n", type=int, default=6)
@@ -800,47 +912,47 @@ def main():
ap.add_argument("--synthetic", type=int, default=0, ap.add_argument("--synthetic", type=int, default=0,
help="number of random ring triangulations to test") help="number of random ring triangulations to test")
ap.add_argument("--seed", type=int, default=1) ap.add_argument("--seed", type=int, default=1)
ap.add_argument("--max-combos", type=int, default=128,
help="cap on diamond-site combinations swept per graph")
ap.add_argument("--verbose", action="store_true") ap.add_argument("--verbose", action="store_true")
args = ap.parse_args() args = ap.parse_args()
import random import random
grand = defaultdict(int) swept_tally = defaultdict(int) # status under the full sweep
first_tally = defaultdict(int) # status under the old first-match rule
sweep = defaultdict(int)
if args.synthetic: if args.synthetic:
rng = random.Random(args.seed) rng = random.Random(args.seed)
tally = defaultdict(int)
for idx in range(args.synthetic): for idx in range(args.synthetic):
sizes, leaf = random_profile(rng) sizes, leaf = random_profile(rng)
g, outer = ring_triangulation(sizes, leaf, rng) g, outer = ring_triangulation(sizes, leaf, rng)
if g is None: if g is None:
tally["error:construction"] += 1 swept_tally["error:construction"] += 1
first_tally["error:construction"] += 1
continue continue
try: try:
res = run_graph(g, outer=outer, verbose=args.verbose) res = run_graph(g, outer=outer, verbose=args.verbose,
max_combos=args.max_combos)
except Exception as ex: # noqa: BLE001 except Exception as ex: # noqa: BLE001
res = f"error:{type(ex).__name__}" res = f"error:{type(ex).__name__}"
tally[res] += 1 status = _record(res, swept_tally, first_tally, sweep)
grand[res] += 1 if args.verbose and status != "ok":
if args.verbose and not res.startswith("ok"): print(f" synth #{idx} sizes={sizes} leaf={leaf}: {status}")
print(f" synth #{idx} sizes={sizes} leaf={leaf}: {res}") print(f"synthetic ({args.synthetic}):")
line = " ".join(f"{k}={v}" for k, v in sorted(tally.items()))
print(f"synthetic ({args.synthetic}): {line}")
else: else:
for n in range(args.min_n, args.max_n + 1): for n in range(args.min_n, args.max_n + 1):
tally = defaultdict(int)
gs = plantri_triangulations(n, args.limit) gs = plantri_triangulations(n, args.limit)
for idx, g in enumerate(gs): for idx, g in enumerate(gs):
try: try:
res = run_graph(g, verbose=args.verbose) res = run_graph(g, verbose=args.verbose,
max_combos=args.max_combos)
except Exception as ex: # noqa: BLE001 except Exception as ex: # noqa: BLE001
res = f"error:{type(ex).__name__}" res = f"error:{type(ex).__name__}"
tally[res] += 1 status = _record(res, swept_tally, first_tally, sweep)
grand[res] += 1 if args.verbose and not status.startswith(("ok", "skip")):
if args.verbose and not res.startswith(("ok", "skip")): print(f" n={n} #{idx}: {status}")
print(f" n={n} #{idx}: {res}")
line = " ".join(f"{k}={v}" for k, v in sorted(tally.items()))
print(f"n={n} ({len(gs)} graphs): {line}")
sys.stdout.flush() sys.stdout.flush()
print("\nTOTAL: " + " ".join(f"{k}={v}" for k, v in sorted(grand.items()))) _print_sweep(sweep, first_tally, swept_tally)
if __name__ == "__main__": if __name__ == "__main__":