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)
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
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**
@@ -38,7 +38,7 @@ import os
import subprocess
import sys
from collections import defaultdict, deque
from itertools import combinations
from itertools import combinations, product
HERE = os.path.dirname(os.path.abspath(__file__))
PLANTRI = os.path.join(HERE, "..", "..", "..", "plantri", "plantri")
@@ -574,55 +574,127 @@ def verify_proper(g, col):
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
g0.check()
if outer is None:
outer = tuple(g0.faces()[0])
an0 = Analysis(g0.copy(), outer)
if an0.degenerate:
return f"skip:{an0.degenerate}"
last = "fail:unknown"
for att in range(attempts):
rng = random.Random(1000 + att)
last = _attempt(g0, outer, rng, verbose)
if last == "ok" or last.startswith("skip"):
return last
return last
orig_vertices = set(g0.rot)
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"
for att in range(attempts):
rng = random.Random(1000 + att)
last = _descend_with_sites(template, outer, orig_vertices,
gadgets, combo, rng)
if last == "ok" or last.startswith("skip"):
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
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 _attempt(g0: Tri, outer, rng, verbose=False):
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()
an = Analysis(g, outer)
if an.degenerate:
return f"skip:{an.degenerate}"
# --- surgeries -------------------------------------------------------
diamonds = [] # (w, quad(a,b,c,d) with diagonal (u,v)) for removal
gadgets = [] # (y, z, u, v, x, t)
gadgets = []
for f in an.terminal:
a, b, c = f
y, z, u, v, x, t = g.insert_leaf_gadget(a, b, c)
gadgets.append((y, z, u, v, x, t))
# re-analyse (gadgets change seams)
an = Analysis(g, outer)
if 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:
if len(cyc) % 2 == 0:
continue
# choose a seam edge whose below-apex is strictly deeper
choice = None
edges = []
for i in range(len(cyc)):
a, b = cyc[i], cyc[(i + 1) % len(cyc)]
x, t = g.apexes_of(a, b)
lx, lt = an.level[x], an.level[t]
if {lx, lt} == {k - 1, k + 1}:
choice = (a, b)
break
if choice is None:
return "skip:no-diamond-site"
w, u, v, x, t = g.insert_diamond(*choice)
edges.append((a, b))
if not edges:
return None
choices.append(edges)
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))
an = Analysis(g, outer)
if an.degenerate:
@@ -636,7 +708,6 @@ def _attempt(g0: Tri, outer, rng, verbose=False):
return f"fail:canonical-{reason}"
# --- descent -----------------------------------------------------------
level = an.level
# diamonds: remove each (diagonal uv)
for w, u, v, x, t in diamonds:
adj = medial_adj(g)
@@ -682,7 +753,7 @@ def _attempt(g0: Tri, outer, rng, verbose=False):
return "fail:gadget-removal"
# --- final check --------------------------------------------------------
if set(g.rot) != set(g0.rot):
if set(g.rot) != orig_vertices:
return "fail:vertex-mismatch"
if not verify_proper(g, col):
return "fail:final-improper"
@@ -792,6 +863,47 @@ def random_profile(rng):
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():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--min-n", type=int, default=6)
@@ -800,47 +912,47 @@ def main():
ap.add_argument("--synthetic", type=int, default=0,
help="number of random ring triangulations to test")
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")
args = ap.parse_args()
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:
rng = random.Random(args.seed)
tally = defaultdict(int)
for idx in range(args.synthetic):
sizes, leaf = random_profile(rng)
g, outer = ring_triangulation(sizes, leaf, rng)
if g is None:
tally["error:construction"] += 1
swept_tally["error:construction"] += 1
first_tally["error:construction"] += 1
continue
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
res = f"error:{type(ex).__name__}"
tally[res] += 1
grand[res] += 1
if args.verbose and not res.startswith("ok"):
print(f" synth #{idx} sizes={sizes} leaf={leaf}: {res}")
line = " ".join(f"{k}={v}" for k, v in sorted(tally.items()))
print(f"synthetic ({args.synthetic}): {line}")
status = _record(res, swept_tally, first_tally, sweep)
if args.verbose and status != "ok":
print(f" synth #{idx} sizes={sizes} leaf={leaf}: {status}")
print(f"synthetic ({args.synthetic}):")
else:
for n in range(args.min_n, args.max_n + 1):
tally = defaultdict(int)
gs = plantri_triangulations(n, args.limit)
for idx, g in enumerate(gs):
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
res = f"error:{type(ex).__name__}"
tally[res] += 1
grand[res] += 1
if args.verbose and not res.startswith(("ok", "skip")):
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}")
status = _record(res, swept_tally, first_tally, sweep)
if args.verbose and not status.startswith(("ok", "skip")):
print(f" n={n} #{idx}: {status}")
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__":