Extend algorithm to even faces, add simple-level-resolution conjecture
- Generalize Phase 1 to include even interior faces as optional flip
candidates and allow the source-triangle break in $L_0$ to be skipped;
generalize Phase 2 so even outer-incident cycles may have at most one
outer-face edge flipped (odd cycles still must have one).
- Define "simple level resolution" as a triangulation $G'$ obtained from
some $(G, S)$ via the algorithm with bipartite parity subgraphs
(Definition 5.4).
- Add Conjecture 5.7 (simple-resolution md4 surjectivity) and
Observation 5.6: every minimum-degree-4 plane triangulation iso-class
on $n \in \{6, ..., 11\}$ vertices is reached as a simple level
resolution. Counts: 1, 1, 2, 5, 12, 34. The md4 restriction is
necessary -- specific non-md4 iso-classes (iso 5 at n=8; iso 25, 183
at n=10) are not reachable.
- Add experiments/simple_level_resolution_coverage.py implementing the
branched algorithm and coverage check, plus supporting scripts for
Phase 1 cycling debugging, Phase 2 gap diagnosis, inductive-lift
scaffolding (inductive_lift_check.py for the route-1 proof strategy),
and visualizations of the unreached n=10 iso-classes and the original
Phase 2 gap example.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+103
@@ -0,0 +1,103 @@
|
||||
"""Compare original Phase 1 (tricky-everywhere only) and new Phase 1
|
||||
(isolated, prefer cross-level) on the same case."""
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import networkx as nx
|
||||
from collections import defaultdict
|
||||
from triangulation_gen import enumerate_all_triangulations
|
||||
from level_cycles import compute_levels
|
||||
from depth_monovariant_check import (
|
||||
faces_of_subgraph, canonical_face, edges_of_face,
|
||||
apex_levels_for_edge, identify_outer_face, face_depths,
|
||||
is_both_k_everywhere, tricky_odd_faces, lowest_depth_neighbor_flip,
|
||||
)
|
||||
from full_resolution_check import (
|
||||
isolated_odd_faces, any_lowest_depth_neighbor_flip,
|
||||
)
|
||||
|
||||
|
||||
def show_apex(face, G, emb, levels, k):
|
||||
pairs = []
|
||||
for (u, v) in edges_of_face(face):
|
||||
r = apex_levels_for_edge(G, emb, u, v, levels)
|
||||
if r is None:
|
||||
pairs.append(None)
|
||||
else:
|
||||
(la, lb), _ = r
|
||||
pairs.append(tuple(sorted([la - k, lb - k])))
|
||||
return pairs
|
||||
|
||||
|
||||
def trace(label, G_initial, levels, nodes_k, k, target_fn, flip_fn,
|
||||
max_steps=15):
|
||||
print(f"\n=== {label} ===")
|
||||
Gc = G_initial.copy()
|
||||
ip, emb_c = nx.check_planarity(Gc)
|
||||
states = []
|
||||
for step in range(max_steps):
|
||||
faces = faces_of_subgraph(Gc, emb_c, nodes_k)
|
||||
outer = identify_outer_face(faces, Gc, emb_c, levels, k)
|
||||
depths = face_depths(faces, outer)
|
||||
targets = target_fn(faces, Gc, emb_c, levels, k, depths, outer)
|
||||
targets_summary = []
|
||||
for cf, f, d in targets:
|
||||
pairs = show_apex(f, Gc, emb_c, levels, k)
|
||||
te = all(p == (0, 0) for p in pairs)
|
||||
targets_summary.append((f, d, "TE" if te else "mixed"))
|
||||
cur_state = tuple(sorted(Gc.subgraph(nodes_k).edges()))
|
||||
repeating = cur_state in states
|
||||
states.append(cur_state)
|
||||
print(f"step {step}: targets={targets_summary} repeat={repeating}")
|
||||
if repeating:
|
||||
print(" CYCLE")
|
||||
return
|
||||
if not targets:
|
||||
print(" done")
|
||||
return
|
||||
cf_t, face_t, d_t = max(targets, key=lambda x: x[2])
|
||||
flip = flip_fn(face_t, Gc, emb_c, levels, k, depths, faces, outer)
|
||||
if flip is None:
|
||||
print(" no flip")
|
||||
return
|
||||
u, v, w, x, _ = flip
|
||||
Gc.remove_edge(u, v)
|
||||
Gc.add_edge(w, x)
|
||||
ip, emb_c = nx.check_planarity(Gc)
|
||||
|
||||
|
||||
tris = enumerate_all_triangulations(9)
|
||||
G = tris[0]
|
||||
source_set = {0, 7, 8}
|
||||
levels = compute_levels(G, source_set)
|
||||
by_level = defaultdict(list)
|
||||
for v, lv in levels.items():
|
||||
by_level[lv].append(v)
|
||||
nodes_k = by_level[1]
|
||||
k = 1
|
||||
print(f"L_1 = {sorted(nodes_k)}, k={k}")
|
||||
print(f"Initial L_1 edges: {sorted(G.subgraph(nodes_k).edges())}")
|
||||
|
||||
# Original Phase 1: tricky-everywhere target
|
||||
def orig_target(faces, G, emb, levels, k, depths, outer):
|
||||
return tricky_odd_faces(faces, G, emb, levels, k, depths, outer)
|
||||
|
||||
|
||||
# Original flip: lowest_depth_neighbor_flip (filters to (k,k) only)
|
||||
def orig_flip(face, G, emb, levels, k, depths, faces, outer):
|
||||
return lowest_depth_neighbor_flip(face, G, emb, levels, k, depths, faces,
|
||||
outer)
|
||||
|
||||
trace("ORIGINAL Phase 1 (tricky-everywhere only, (k,k) flips)",
|
||||
G, levels, nodes_k, k, orig_target, orig_flip)
|
||||
|
||||
|
||||
def new_target(faces, G, emb, levels, k, depths, outer):
|
||||
return isolated_odd_faces(faces, depths, outer)
|
||||
|
||||
|
||||
def new_flip(face, G, emb, levels, k, depths, faces, outer):
|
||||
return any_lowest_depth_neighbor_flip(face, G, emb, levels, k, depths,
|
||||
faces, outer)
|
||||
|
||||
trace("NEW Phase 1 (isolated, prefer cross-level)",
|
||||
G, levels, nodes_k, k, new_target, new_flip)
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Trace one budget-failure case to see if Phase 1 (new) cycles."""
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import networkx as nx
|
||||
from collections import defaultdict
|
||||
from triangulation_gen import enumerate_all_triangulations
|
||||
from level_cycles import compute_levels
|
||||
from depth_monovariant_check import (
|
||||
faces_of_subgraph, canonical_face, edges_of_face,
|
||||
apex_levels_for_edge, identify_outer_face, face_depths,
|
||||
)
|
||||
from full_resolution_check import (
|
||||
isolated_odd_faces, any_lowest_depth_neighbor_flip,
|
||||
)
|
||||
|
||||
|
||||
tris = enumerate_all_triangulations(9)
|
||||
G = tris[0]
|
||||
source_set = {0, 7, 8}
|
||||
levels = compute_levels(G, source_set)
|
||||
by_level = defaultdict(list)
|
||||
for v, lv in levels.items():
|
||||
by_level[lv].append(v)
|
||||
nodes_k = by_level[1]
|
||||
k = 1
|
||||
|
||||
print(f"L_1 = {sorted(nodes_k)}")
|
||||
print(f"L_1 initial edges: {sorted(G.subgraph(nodes_k).edges())}")
|
||||
|
||||
Gc = G.copy()
|
||||
ip, emb_c = nx.check_planarity(Gc)
|
||||
history = []
|
||||
for step in range(15):
|
||||
faces = faces_of_subgraph(Gc, emb_c, nodes_k)
|
||||
outer = identify_outer_face(faces, Gc, emb_c, levels, k)
|
||||
depths = face_depths(faces, outer)
|
||||
isolated = isolated_odd_faces(faces, depths, outer)
|
||||
iso_summary = [(f, d) for cf, f, d in isolated]
|
||||
edges = tuple(sorted(Gc.subgraph(nodes_k).edges()))
|
||||
print(f"\nstep {step}: |L_1 edges|={len(edges)} isolated_odd={iso_summary}")
|
||||
if not isolated:
|
||||
print(" done")
|
||||
break
|
||||
cf_t, face_t, d_t = max(isolated, key=lambda x: x[2])
|
||||
flip = any_lowest_depth_neighbor_flip(
|
||||
face_t, Gc, emb_c, levels, k, depths, faces, outer)
|
||||
if flip is None:
|
||||
print(" no flip")
|
||||
break
|
||||
u, v, w, x, od = flip
|
||||
apex = apex_levels_for_edge(Gc, emb_c, u, v, levels)
|
||||
(la, lb), _ = apex
|
||||
pair = tuple(sorted([la - k, lb - k]))
|
||||
print(f" flip face {face_t} d={d_t}: ({u},{v})[apex {pair}] -> ({w},{x}), neighbor_depth={od}")
|
||||
if edges in [h[0] for h in history]:
|
||||
prior = next(h for h in history if h[0] == edges)
|
||||
print(f" *** REPEATING state from step {prior[1]} ***")
|
||||
break
|
||||
history.append((edges, step))
|
||||
Gc.remove_edge(u, v)
|
||||
Gc.add_edge(w, x)
|
||||
ip, emb_c = nx.check_planarity(Gc)
|
||||
if not ip:
|
||||
print(" nonplanar"); break
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Inspect one Phase 2 failure to see why an odd cycle survived."""
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import networkx as nx
|
||||
from collections import defaultdict
|
||||
from triangulation_gen import enumerate_all_triangulations
|
||||
from level_cycles import compute_levels
|
||||
from depth_monovariant_check import (
|
||||
faces_of_subgraph, canonical_face, edges_of_face,
|
||||
apex_levels_for_edge, identify_outer_face, face_depths,
|
||||
tricky_odd_faces, lowest_depth_neighbor_flip,
|
||||
)
|
||||
from full_resolution_check import (
|
||||
phase1_resolve_tricky, phase2_flip_outer_incident, is_Lk_bipartite,
|
||||
)
|
||||
|
||||
|
||||
tris = enumerate_all_triangulations(10)
|
||||
G = tris[13]
|
||||
source_set = {1, 2, 4}
|
||||
levels = compute_levels(G, source_set)
|
||||
by_level = defaultdict(list)
|
||||
for v, lv in levels.items():
|
||||
by_level[lv].append(v)
|
||||
nodes_k = by_level[1]
|
||||
k = 1
|
||||
|
||||
print("INITIAL G")
|
||||
print(f" L_1 = {sorted(nodes_k)}")
|
||||
Lk0 = G.subgraph(nodes_k)
|
||||
print(f" L_1 edges: {sorted(Lk0.edges())}")
|
||||
print(f" L_1 bipartite? {nx.is_bipartite(Lk0)}")
|
||||
|
||||
print("\nRunning Phase 1...")
|
||||
G1, emb1, p1_steps, p1_reason = phase1_resolve_tricky(G, levels, nodes_k, k)
|
||||
print(f" Phase 1: {p1_steps} steps, reason={p1_reason}")
|
||||
Lk1 = G1.subgraph(nodes_k)
|
||||
print(f" L_1 after Phase 1: edges = {sorted(Lk1.edges())}")
|
||||
print(f" L_1 bipartite? {nx.is_bipartite(Lk1)}")
|
||||
|
||||
faces = faces_of_subgraph(G1, emb1, nodes_k)
|
||||
outer = identify_outer_face(faces, G1, emb1, levels, k)
|
||||
outer_face = next(f for f in faces if canonical_face(f) == outer)
|
||||
print(f"\n outer face: {outer_face}")
|
||||
print(f" faces of L_1:")
|
||||
outer_edges = set(frozenset(list(e)) for e in edges_of_face(outer_face))
|
||||
for f in faces:
|
||||
cf = canonical_face(f)
|
||||
if cf == outer:
|
||||
continue
|
||||
incident = any(frozenset([a, b]) in outer_edges
|
||||
for (a, b) in edges_of_face(f))
|
||||
odd = len(f) % 2 == 1
|
||||
print(f" {f}: len={len(f)} odd={odd} incident_to_outer={incident}")
|
||||
|
||||
# Enumerate ALL simple cycles of L_1 (not just face-cycles)
|
||||
print(f"\n all simple cycles of L_1 (up to length 6):")
|
||||
def find_cycles(G, max_len):
|
||||
cycles = set()
|
||||
for u in G.nodes():
|
||||
for v in G.neighbors(u):
|
||||
for path in nx.all_simple_paths(G, v, u, cutoff=max_len-1):
|
||||
if len(path) >= 3:
|
||||
c = tuple(sorted(path))
|
||||
cycles.add(c)
|
||||
return cycles
|
||||
for cyc in sorted(find_cycles(Lk1, 6)):
|
||||
if len(cyc) % 2 == 1:
|
||||
print(f" {cyc} (odd, len {len(cyc)})")
|
||||
|
||||
print("\nRunning Phase 2...")
|
||||
G2, flips = phase2_flip_outer_incident(G1, emb1, levels, nodes_k, k)
|
||||
print(f" flips: {flips}")
|
||||
Lk2 = G2.subgraph(nodes_k)
|
||||
print(f" L_1 after Phase 2: edges = {sorted(Lk2.edges())}")
|
||||
print(f" L_1 bipartite? {nx.is_bipartite(Lk2)}")
|
||||
print(f"\n all odd simple cycles of final L_1 (up to length 6):")
|
||||
for cyc in sorted(find_cycles(Lk2, 6)):
|
||||
if len(cyc) % 2 == 1:
|
||||
print(f" {cyc}")
|
||||
+278
@@ -0,0 +1,278 @@
|
||||
"""
|
||||
Two-phase resolution algorithm test:
|
||||
Phase 1: depth-guided flip loop to resolve tricky-everywhere odd cycles.
|
||||
Phase 2: for each odd simple cycle of L_k incident to the outer face,
|
||||
flip one (outer-incident) edge. Multi-edges allowed; we work in
|
||||
the simple-graph collapse where add_edge on an existing edge is
|
||||
a no-op (which is equivalent to multigraph bipartiteness).
|
||||
|
||||
Success criterion: at the end, the induced subgraph on each level k is
|
||||
bipartite (no odd cycle).
|
||||
"""
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import networkx as nx
|
||||
from collections import defaultdict
|
||||
from triangulation_gen import enumerate_all_triangulations
|
||||
from level_cycles import compute_levels, level_sources
|
||||
from depth_monovariant_check import (
|
||||
faces_of_subgraph, canonical_face, edges_of_face,
|
||||
apex_levels_for_edge, identify_outer_face, face_depths,
|
||||
)
|
||||
|
||||
|
||||
def outer_edge_count(face, outer_face_edges):
|
||||
return sum(1 for (a, b) in edges_of_face(face)
|
||||
if frozenset([a, b]) in outer_face_edges)
|
||||
|
||||
|
||||
def interior_odd_faces(faces, depths, outer_canon, outer_face_edges):
|
||||
"""Odd inner faces with depth > 0 AND no edges incident to the outer face."""
|
||||
out = []
|
||||
for face in faces:
|
||||
cf = canonical_face(face)
|
||||
if cf == outer_canon:
|
||||
continue
|
||||
if len(face) % 2 != 1:
|
||||
continue
|
||||
d = depths.get(cf)
|
||||
if d is None or d <= 0:
|
||||
continue
|
||||
if outer_edge_count(face, outer_face_edges) > 0:
|
||||
continue
|
||||
out.append((cf, face, d))
|
||||
return out
|
||||
|
||||
|
||||
def shared_edge_to_lowest_neighbor(face, G, emb_G, levels, k, depths, faces,
|
||||
outer_canon):
|
||||
"""Find the inner face F' adjacent to `face` with minimum facial depth,
|
||||
and return the flip on the edge shared between face and F'."""
|
||||
face_canon = canonical_face(face)
|
||||
face_edges = set(frozenset([a, b]) for (a, b) in edges_of_face(face))
|
||||
best = None # (other_depth, u, v, w, x)
|
||||
for f in faces:
|
||||
cf = canonical_face(f)
|
||||
if cf == face_canon or cf == outer_canon:
|
||||
continue
|
||||
# Find shared edge between face and f.
|
||||
shared = None
|
||||
for (a, b) in edges_of_face(f):
|
||||
if frozenset([a, b]) in face_edges:
|
||||
shared = (a, b)
|
||||
break
|
||||
if shared is None:
|
||||
continue
|
||||
other_depth = depths.get(cf)
|
||||
if other_depth is None:
|
||||
continue
|
||||
u, v = shared
|
||||
if not G.has_edge(u, v):
|
||||
continue
|
||||
result = apex_levels_for_edge(G, emb_G, u, v, levels)
|
||||
if result is None:
|
||||
continue
|
||||
_, (w, x) = result
|
||||
if G.has_edge(w, x):
|
||||
continue
|
||||
if best is None or other_depth < best[0]:
|
||||
best = (other_depth, u, v, w, x)
|
||||
if best is None:
|
||||
return None
|
||||
_, u, v, w, x = best
|
||||
return (u, v, w, x)
|
||||
|
||||
|
||||
def phase1_resolve_isolated(G, levels, nodes_k, k, max_steps=400):
|
||||
"""Per spec:
|
||||
(1) take the dual of L_k;
|
||||
(2) depth 0 = inner face with >=2 outer-face edges;
|
||||
(3) other faces' depth = BFS distance to a depth-0 face;
|
||||
(4) find odd C of greatest depth with 0 outer-face edges;
|
||||
(5) flip the edge shared with the lowest-depth incident neighbor.
|
||||
Repeat until no such C exists."""
|
||||
Gc = G.copy()
|
||||
ip, emb_c = nx.check_planarity(Gc)
|
||||
for step in range(max_steps):
|
||||
faces = faces_of_subgraph(Gc, emb_c, nodes_k)
|
||||
if len(faces) < 2:
|
||||
return Gc, emb_c, step, 'no_faces'
|
||||
outer = identify_outer_face(faces, Gc, emb_c, levels, k)
|
||||
if outer is None:
|
||||
return Gc, emb_c, step, 'no_outer'
|
||||
outer_face = next((f for f in faces if canonical_face(f) == outer),
|
||||
None)
|
||||
outer_face_edges = set()
|
||||
if outer_face is not None:
|
||||
for (a, b) in edges_of_face(outer_face):
|
||||
outer_face_edges.add(frozenset([a, b]))
|
||||
depths = face_depths(faces, outer)
|
||||
if depths is None:
|
||||
depths = {}
|
||||
interior = interior_odd_faces(faces, depths, outer, outer_face_edges)
|
||||
if not interior:
|
||||
return Gc, emb_c, step, 'done'
|
||||
cf_t, face_t, d_t = max(interior, key=lambda x: x[2])
|
||||
flip = shared_edge_to_lowest_neighbor(
|
||||
face_t, Gc, emb_c, levels, k, depths, faces, outer)
|
||||
if flip is None:
|
||||
return Gc, emb_c, step, 'no_flip'
|
||||
u, v, w, x = flip
|
||||
Gc.remove_edge(u, v)
|
||||
Gc.add_edge(w, x)
|
||||
ip, emb_c = nx.check_planarity(Gc)
|
||||
if not ip:
|
||||
return Gc, emb_c, step, 'nonplanar'
|
||||
return Gc, emb_c, max_steps, 'budget'
|
||||
|
||||
|
||||
def phase2_flip_outer_incident(G, emb, levels, nodes_k, k):
|
||||
"""Flip one edge per odd simple cycle of L_k incident to outer face.
|
||||
Multi-edges allowed (simple-graph add is a no-op, equivalent to multigraph
|
||||
bipartiteness)."""
|
||||
faces = faces_of_subgraph(G, emb, nodes_k)
|
||||
if not faces:
|
||||
return G.copy(), []
|
||||
outer = identify_outer_face(faces, G, emb, levels, k)
|
||||
if outer is None:
|
||||
return G.copy(), []
|
||||
outer_face = next((f for f in faces if canonical_face(f) == outer), None)
|
||||
if outer_face is None:
|
||||
return G.copy(), []
|
||||
outer_edges = set(frozenset(list(e)) for e in edges_of_face(outer_face))
|
||||
Gn = G.copy()
|
||||
flips = []
|
||||
for face in faces:
|
||||
cf = canonical_face(face)
|
||||
if cf == outer:
|
||||
continue
|
||||
if len(face) % 2 != 1:
|
||||
continue
|
||||
# Incident to outer face?
|
||||
face_edges = [frozenset([a, b]) for (a, b) in edges_of_face(face)]
|
||||
if not any(e in outer_edges for e in face_edges):
|
||||
continue
|
||||
# Prefer the outer-incident edge for the flip.
|
||||
chosen = None
|
||||
for (u, v) in edges_of_face(face):
|
||||
if frozenset([u, v]) not in outer_edges:
|
||||
continue
|
||||
if not Gn.has_edge(u, v):
|
||||
continue
|
||||
result = apex_levels_for_edge(Gn, emb, u, v, levels)
|
||||
if result is None:
|
||||
continue
|
||||
_, (w, x) = result
|
||||
chosen = (u, v, w, x)
|
||||
break
|
||||
if chosen is None:
|
||||
continue
|
||||
u, v, w, x = chosen
|
||||
Gn.remove_edge(u, v)
|
||||
# add_edge on existing edge in nx.Graph is a no-op (multigraph collapse)
|
||||
Gn.add_edge(w, x)
|
||||
flips.append((u, v, w, x))
|
||||
return Gn, flips
|
||||
|
||||
|
||||
def is_Lk_bipartite(G, nodes_k):
|
||||
Lk = G.subgraph(nodes_k)
|
||||
return nx.is_bipartite(Lk)
|
||||
|
||||
|
||||
def run_check(n_values, max_steps=200):
|
||||
total = 0
|
||||
p1_failed = 0
|
||||
p2_success = 0
|
||||
p2_failure = 0
|
||||
parity_bipartite = 0
|
||||
failures = []
|
||||
|
||||
for n in n_values:
|
||||
print(f"\n=== n = {n} ===")
|
||||
tris = enumerate_all_triangulations(n)
|
||||
print(f" {len(tris)} iso-classes")
|
||||
for tri_idx, G in enumerate(tris):
|
||||
ip, emb = nx.check_planarity(G)
|
||||
if not ip:
|
||||
continue
|
||||
for kind, label, source_set in level_sources(G, emb):
|
||||
levels = compute_levels(G, source_set)
|
||||
by_level = defaultdict(list)
|
||||
for v, lv in levels.items():
|
||||
by_level[lv].append(v)
|
||||
# Run on every level k>=1 with at least one odd cycle.
|
||||
for k, nodes_k in by_level.items():
|
||||
if k == 0 or len(nodes_k) < 3:
|
||||
continue
|
||||
faces = faces_of_subgraph(G, emb, nodes_k)
|
||||
if len(faces) < 2:
|
||||
continue
|
||||
has_odd = any(
|
||||
len(f) % 2 == 1
|
||||
and canonical_face(f) != canonical_face(
|
||||
max(faces, key=len))
|
||||
for f in faces
|
||||
)
|
||||
if not has_odd:
|
||||
continue
|
||||
total += 1
|
||||
G1, emb1, p1_steps, p1_reason = phase1_resolve_isolated(
|
||||
G, levels, nodes_k, k, max_steps)
|
||||
if p1_reason not in ('done', 'no_faces'):
|
||||
p1_failed += 1
|
||||
if len(failures) < 5:
|
||||
failures.append({
|
||||
'n': n, 'tri_idx': tri_idx,
|
||||
'source': (kind, label),
|
||||
'k': k,
|
||||
'phase': 1, 'reason': p1_reason
|
||||
})
|
||||
continue
|
||||
G2, flips = phase2_flip_outer_incident(
|
||||
G1, emb1, levels, nodes_k, k)
|
||||
if is_Lk_bipartite(G2, nodes_k):
|
||||
p2_success += 1
|
||||
# Also check the parity subgraph bipartiteness.
|
||||
# Parity = even / odd in BFS levels.
|
||||
even_nodes = [v for v in G2.nodes()
|
||||
if levels.get(v, 0) % 2 == 0]
|
||||
odd_nodes = [v for v in G2.nodes()
|
||||
if levels.get(v, 0) % 2 == 1]
|
||||
even_sub = G2.subgraph(even_nodes)
|
||||
odd_sub = G2.subgraph(odd_nodes)
|
||||
if nx.is_bipartite(even_sub) and \
|
||||
nx.is_bipartite(odd_sub):
|
||||
parity_bipartite += 1
|
||||
else:
|
||||
p2_failure += 1
|
||||
if len(failures) < 5:
|
||||
Lk_after = G2.subgraph(nodes_k)
|
||||
failures.append({
|
||||
'n': n, 'tri_idx': tri_idx,
|
||||
'source': (kind, label),
|
||||
'k': k, 'phase': 2,
|
||||
'reason': 'Lk_not_bipartite',
|
||||
'flips': flips,
|
||||
'Lk_edges': sorted(Lk_after.edges()),
|
||||
})
|
||||
print(f"\n=== summary ===")
|
||||
print(f"configs with odd cycles in some L_k: {total}")
|
||||
print(f" Phase 1 succeeded: {total - p1_failed}")
|
||||
print(f" Phase 1 failed: {p1_failed}")
|
||||
print(f" Phase 2 -> L_k bipartite: {p2_success}")
|
||||
print(f" Phase 2 -> L_k still has odd cycle: {p2_failure}")
|
||||
print(f" Of bipartite L_k: full parity-subgraph bipartite: "
|
||||
f"{parity_bipartite}")
|
||||
if failures:
|
||||
print(f"\nfailures (first {len(failures)}):")
|
||||
for f in failures:
|
||||
print(f" {f}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1:
|
||||
ns = [int(x) for x in sys.argv[1:]]
|
||||
else:
|
||||
ns = [9, 10]
|
||||
run_check(ns)
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
"""Hand-check scaffold for the inductive lift step (route 1).
|
||||
|
||||
For each md4 iso-class T at n:
|
||||
1. Pick a degree-4 vertex v.
|
||||
2. Contract v: delete v and retriangulate the resulting 4-cycle with one
|
||||
of two possible diagonals. This gives T' on n-1 vertices.
|
||||
3. Identify T's iso-class at n-1.
|
||||
4. Find empirical (G', S') such that the algorithm produces T'.
|
||||
5. Lift G' to G on n vertices by reinserting v in a corresponding face.
|
||||
6. Run the algorithm on (G, S) and check if it produces T.
|
||||
|
||||
Reports what worked / failed for each pair (md4 target, degree-4 vertex).
|
||||
"""
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import networkx as nx
|
||||
from collections import defaultdict
|
||||
from triangulation_gen import enumerate_all_triangulations
|
||||
from level_cycles import compute_levels, level_sources
|
||||
from simple_level_resolution_coverage import (
|
||||
simple_level_resolutions, is_triangulation, iso_index, is_self_level_resolution,
|
||||
)
|
||||
|
||||
|
||||
def cyclic_neighbors(G, v):
|
||||
"""Return the cyclic order of v's neighbors in G's planar embedding."""
|
||||
ip, emb = nx.check_planarity(G)
|
||||
if not ip:
|
||||
return None
|
||||
return list(emb.neighbors_cw_order(v))
|
||||
|
||||
|
||||
def contract_degree4_vertex(G, v):
|
||||
"""Remove a degree-4 vertex v from G and retriangulate the 4-cycle
|
||||
formed by its neighbors. Yield (T', diagonal) for each of the two
|
||||
possible diagonal choices."""
|
||||
cyc = cyclic_neighbors(G, v)
|
||||
if cyc is None or len(cyc) != 4:
|
||||
return
|
||||
a, b, c, d = cyc
|
||||
for (u, w) in [(a, c), (b, d)]:
|
||||
Gp = G.copy()
|
||||
Gp.remove_node(v)
|
||||
if Gp.has_edge(u, w):
|
||||
continue
|
||||
Gp.add_edge(u, w)
|
||||
ip, _ = nx.check_planarity(Gp)
|
||||
if not ip:
|
||||
continue
|
||||
if Gp.number_of_edges() != 3 * (Gp.number_of_nodes()) - 6:
|
||||
continue
|
||||
yield Gp, (u, w), (a, b, c, d)
|
||||
|
||||
|
||||
def find_labeled_preimage(target_labeled, isos):
|
||||
"""Find any (G, S) at n vertices such that algorithm yields a graph
|
||||
EXACTLY equal (as labeled simple graph) to target_labeled.
|
||||
To get all possible labelings, we search over the iso-class enumeration
|
||||
plus all relabeling isomorphisms.
|
||||
Returns (G, source_set, Gp_out) or None."""
|
||||
target_edges = set(frozenset(e) for e in target_labeled.edges())
|
||||
target_nodes = set(target_labeled.nodes())
|
||||
n = len(target_nodes)
|
||||
for G in isos:
|
||||
# Try all label mappings from G's vertices to target_labeled's vertices.
|
||||
# For small n this is feasible; otherwise we'd use isomorphism enum.
|
||||
gm = nx.isomorphism.GraphMatcher(G, target_labeled) # not what we want
|
||||
# We want to find an iso from G to *some* labeled triangulation
|
||||
# H, where algorithm(H, S) = target_labeled exactly.
|
||||
# Easier: just enumerate label permutations of G's vertices and
|
||||
# check each.
|
||||
from itertools import permutations
|
||||
# Use permutations only if n is small (n<=7 is fine, n=8 = 40k perms)
|
||||
if n > 7:
|
||||
continue
|
||||
for perm in permutations(target_nodes, n):
|
||||
relabel = {orig: perm[i] for i, orig in enumerate(sorted(G.nodes()))}
|
||||
H = nx.relabel_nodes(G, relabel, copy=True)
|
||||
ip, emb = nx.check_planarity(H)
|
||||
if not ip:
|
||||
continue
|
||||
for kind, lab, source_set in level_sources(H, emb):
|
||||
for Hp in simple_level_resolutions(H, source_set):
|
||||
if not is_triangulation(Hp, n):
|
||||
continue
|
||||
Hp_edges = set(frozenset(e) for e in Hp.edges())
|
||||
if Hp_edges == target_edges:
|
||||
return (H, source_set, Hp)
|
||||
return None
|
||||
|
||||
|
||||
def insert_vertex_into_face(G, face_vertices, new_v):
|
||||
"""Insert new_v into the triangular face of G defined by face_vertices.
|
||||
Returns the new graph. The face must currently be a triangle."""
|
||||
Gp = G.copy()
|
||||
Gp.add_node(new_v)
|
||||
for u in face_vertices:
|
||||
Gp.add_edge(new_v, u)
|
||||
return Gp
|
||||
|
||||
|
||||
def is_md4(T):
|
||||
return min(T.degree(v) for v in T.nodes()) >= 4
|
||||
|
||||
|
||||
def attempt_lift(G_prime, target_T, cyc, diag, v_label):
|
||||
"""Attempt to lift G' to G on n vertices by reinserting v.
|
||||
|
||||
Idea: if the diagonal (u, w) is in G_prime and (u, w) borders two
|
||||
triangles in G_prime, we can "uncontract" by removing the diagonal
|
||||
and inserting v adjacent to the 4 boundary vertices of those two
|
||||
triangles. The 4 boundary vertices should match cyc up to rotation
|
||||
+ reflection.
|
||||
|
||||
Returns G (the lifted graph) if successful, else None.
|
||||
"""
|
||||
u, w = diag
|
||||
if not G_prime.has_edge(u, w):
|
||||
return None, "diagonal not in G'"
|
||||
ip, emb = nx.check_planarity(G_prime)
|
||||
if not ip:
|
||||
return None, "G' not planar"
|
||||
f1 = emb.traverse_face(u, w)
|
||||
f2 = emb.traverse_face(w, u)
|
||||
if len(f1) != 3 or len(f2) != 3:
|
||||
return None, "diagonal not bordered by two triangles"
|
||||
x = next(p for p in f1 if p != u and p != w)
|
||||
y = next(p for p in f2 if p != u and p != w)
|
||||
boundary = {u, w, x, y}
|
||||
if boundary != set(cyc):
|
||||
return None, f"boundary {boundary} != cyc {set(cyc)}"
|
||||
G = G_prime.copy()
|
||||
G.remove_edge(u, w)
|
||||
G.add_node(v_label)
|
||||
for vert in boundary:
|
||||
G.add_edge(v_label, vert)
|
||||
if G.number_of_edges() != 3 * G.number_of_nodes() - 6:
|
||||
return None, f"wrong edge count {G.number_of_edges()}"
|
||||
ip2, _ = nx.check_planarity(G)
|
||||
if not ip2:
|
||||
return None, "lifted G not planar"
|
||||
return G, "ok"
|
||||
|
||||
|
||||
def main():
|
||||
n = 7
|
||||
isos_n = enumerate_all_triangulations(n)
|
||||
isos_n1 = enumerate_all_triangulations(n - 1)
|
||||
|
||||
md4_iso = [i for i, T in enumerate(isos_n) if is_md4(T)]
|
||||
md4_iso_n1 = [i for i, T in enumerate(isos_n1) if is_md4(T)]
|
||||
print(f"n={n}: md4 iso-classes = {md4_iso}")
|
||||
print(f"n={n-1}: md4 iso-classes = {md4_iso_n1}")
|
||||
|
||||
for tgt_idx in md4_iso:
|
||||
T = isos_n[tgt_idx]
|
||||
print(f"\n=== Target T = iso[{tgt_idx}] at n={n} ===")
|
||||
deg = {v: T.degree(v) for v in T.nodes()}
|
||||
deg4_vertices = [v for v, d in deg.items() if d == 4]
|
||||
|
||||
good_contractions = []
|
||||
for v in deg4_vertices:
|
||||
for Tp, diag, cyc in contract_degree4_vertex(T, v):
|
||||
tp_idx = iso_index(Tp, isos_n1)
|
||||
if tp_idx is None:
|
||||
continue
|
||||
is_good = tp_idx in md4_iso_n1
|
||||
marker = "GOOD (md4)" if is_good else "non-md4"
|
||||
print(f" v={v} diag={diag} cyc={cyc}: T' = iso[{tp_idx}] {marker}")
|
||||
if is_good:
|
||||
good_contractions.append((v, diag, cyc, Tp, tp_idx))
|
||||
|
||||
if not good_contractions:
|
||||
print(f" ! NO md4-preserving contraction found")
|
||||
continue
|
||||
|
||||
v, diag, cyc, Tp, tp_idx = good_contractions[0]
|
||||
print(f"\n Trying lift via good contraction: v={v} diag={diag}")
|
||||
print(f" T' (labeled, vertices {sorted(Tp.nodes())}): "
|
||||
f"edges {sorted(Tp.edges())}")
|
||||
# Search for a LABELED preimage of T'.
|
||||
labeled_pre = find_labeled_preimage(Tp, isos_n1)
|
||||
if labeled_pre is None:
|
||||
print(f" no labeled preimage found for T'")
|
||||
continue
|
||||
H, source_set, Hp = labeled_pre
|
||||
print(f" labeled preimage H: edges {sorted(H.edges())}")
|
||||
print(f" source: {sorted(source_set)}")
|
||||
print(f" algorithm(H, S) yields T' exactly (labels match)")
|
||||
# Now lift H to G by uncontracting at the diagonal
|
||||
G_lifted, status = attempt_lift(H, T, cyc, diag, v)
|
||||
print(f" lift status: {status}")
|
||||
if G_lifted is None:
|
||||
return
|
||||
print(f" lifted G edges: {sorted(G_lifted.edges())}")
|
||||
# Run algorithm on G_lifted with the same source set
|
||||
success = False
|
||||
for Gp_run in simple_level_resolutions(G_lifted, source_set):
|
||||
if not is_triangulation(Gp_run, n):
|
||||
continue
|
||||
if nx.is_isomorphic(Gp_run, T):
|
||||
# Check exact label match too
|
||||
if set(frozenset(e) for e in Gp_run.edges()) == \
|
||||
set(frozenset(e) for e in T.edges()):
|
||||
print(f" SUCCESS (label-exact): algorithm yields T")
|
||||
else:
|
||||
print(f" SUCCESS (iso, not label-exact): algorithm "
|
||||
f"yields a graph iso to T")
|
||||
success = True
|
||||
break
|
||||
if not success:
|
||||
print(f" lift OK but algorithm on (G, S) does not yield T")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Inspect iso-class 5 at n=8 to see what's structurally special."""
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import networkx as nx
|
||||
from collections import Counter
|
||||
from triangulation_gen import enumerate_all_triangulations
|
||||
|
||||
|
||||
isos = enumerate_all_triangulations(8)
|
||||
T5 = isos[5]
|
||||
print(f"iso-class 5 at n=8:")
|
||||
print(f" vertices: {sorted(T5.nodes())}")
|
||||
print(f" edges: {sorted(T5.edges())}")
|
||||
print(f" degree sequence: {sorted([T5.degree(v) for v in T5.nodes()], reverse=True)}")
|
||||
print(f" min degree: {min(T5.degree(v) for v in T5.nodes())}")
|
||||
|
||||
ip, emb = nx.check_planarity(T5)
|
||||
print(f" planar: {ip}")
|
||||
|
||||
# What face cycles exist?
|
||||
faces = []
|
||||
seen = set()
|
||||
for v in emb.nodes():
|
||||
for w in emb[v]:
|
||||
if (v, w) not in seen:
|
||||
f = emb.traverse_face(v, w)
|
||||
for i in range(len(f)):
|
||||
seen.add((f[i], f[(i+1) % len(f)]))
|
||||
faces.append(f)
|
||||
print(f" face lengths: {Counter(len(f) for f in faces)}")
|
||||
|
||||
# Check 4-colorability and best matching
|
||||
def four_color(G):
|
||||
"""Greedy 4-coloring."""
|
||||
colors = {}
|
||||
for v in nx.nodes(G):
|
||||
used = {colors[u] for u in G.neighbors(v) if u in colors}
|
||||
for c in range(4):
|
||||
if c not in used:
|
||||
colors[v] = c; break
|
||||
return colors
|
||||
|
||||
# Check whether iso 5 can be 4-colored
|
||||
try:
|
||||
col = four_color(T5)
|
||||
sizes = Counter(col.values())
|
||||
print(f" greedy 4-coloring sizes: {sizes}")
|
||||
except Exception as e:
|
||||
print(f" 4-coloring failed: {e}")
|
||||
|
||||
# Comparison: print all 14 iso-classes' degree sequences
|
||||
print(f"\nAll iso-classes at n=8 (degree sequences, sorted):")
|
||||
for i, T in enumerate(isos):
|
||||
ds = tuple(sorted([T.degree(v) for v in T.nodes()], reverse=True))
|
||||
marker = " <-- missing" if i in [5] else ""
|
||||
print(f" {i:2d}: {ds}{marker}")
|
||||
|
||||
# Now check if any algorithm output has degree sequence (6,6,6,5,4,3,3,3)
|
||||
print(f"\nChecking algorithm outputs at n=8 for target degree sequence...")
|
||||
from level_cycles import level_sources, compute_levels
|
||||
from simple_level_resolution_coverage import simple_level_resolutions, is_triangulation
|
||||
target_ds = tuple(sorted([T5.degree(v) for v in T5.nodes()], reverse=True))
|
||||
found = 0
|
||||
total = 0
|
||||
for src_idx, G in enumerate(isos):
|
||||
ip, emb = nx.check_planarity(G)
|
||||
if not ip:
|
||||
continue
|
||||
for kind, label, source_set in level_sources(G, emb):
|
||||
for Gp in simple_level_resolutions(G, source_set):
|
||||
total += 1
|
||||
if not is_triangulation(Gp, 8):
|
||||
continue
|
||||
ds = tuple(sorted([Gp.degree(v) for v in Gp.nodes()], reverse=True))
|
||||
if ds == target_ds:
|
||||
found += 1
|
||||
if found <= 3:
|
||||
print(f" found! src={src_idx} source={label}")
|
||||
print(f" Gp edges: {sorted(Gp.edges())}")
|
||||
print(f"total outputs: {total}, with target degree sequence: {found}")
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Plot iso-classes 25 and 183 at n=10 (the two unreachable iso-classes)."""
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import networkx as nx
|
||||
import matplotlib.pyplot as plt
|
||||
from collections import Counter
|
||||
from triangulation_gen import enumerate_all_triangulations
|
||||
|
||||
|
||||
isos = enumerate_all_triangulations(10)
|
||||
targets = [25, 183]
|
||||
|
||||
fig, axes = plt.subplots(1, 2, figsize=(14, 7))
|
||||
for ax, idx in zip(axes, targets):
|
||||
T = isos[idx]
|
||||
ip, emb = nx.check_planarity(T)
|
||||
pos = nx.combinatorial_embedding_to_pos(emb)
|
||||
nx.draw_networkx_edges(T, pos, ax=ax, edge_color='black', width=1.0)
|
||||
nx.draw_networkx_nodes(T, pos, ax=ax, node_color='lightblue',
|
||||
node_size=380, edgecolors='black')
|
||||
nx.draw_networkx_labels(T, pos, ax=ax, font_size=10)
|
||||
ds = sorted([T.degree(v) for v in T.nodes()], reverse=True)
|
||||
dcounts = Counter(ds)
|
||||
ds_str = ", ".join(f"{d}^{c}" if c > 1 else str(d)
|
||||
for d, c in sorted(dcounts.items(), reverse=True))
|
||||
ax.set_title(f"n=10 iso-class {idx}\n"
|
||||
f"degree sequence: {ds_str}\n"
|
||||
f"min degree: {min(ds)}, max: {max(ds)}",
|
||||
fontsize=11)
|
||||
ax.set_aspect('equal')
|
||||
ax.axis('off')
|
||||
|
||||
plt.tight_layout()
|
||||
out = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
'..', 'unreached_iso_n10.png')
|
||||
plt.savefig(out, dpi=130, bbox_inches='tight')
|
||||
print(f"saved: {out}")
|
||||
|
||||
# Also print structural details
|
||||
print(f"\niso 25 edges: {sorted(isos[25].edges())}")
|
||||
print(f"iso 183 edges: {sorted(isos[183].edges())}")
|
||||
print(f"\niso 25 degree seq: {sorted([isos[25].degree(v) for v in isos[25].nodes()], reverse=True)}")
|
||||
print(f"iso 183 degree seq: {sorted([isos[183].degree(v) for v in isos[183].nodes()], reverse=True)}")
|
||||
+438
@@ -0,0 +1,438 @@
|
||||
"""Simple level resolution coverage check.
|
||||
|
||||
For each (G, S) where G is a triangulation on n vertices, run Phase 1 +
|
||||
Phase 2 across all levels k>=1 (in order k=1, 2, ...). Record the resulting
|
||||
G' iso-class IF G' is still a triangulation (3n-6 edges, no multi-edges,
|
||||
planar). Aggregate across all (G, S) to see which iso-classes are reachable
|
||||
as simple level resolutions.
|
||||
|
||||
Question: is every triangulation iso-class on n vertices reached?
|
||||
"""
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import networkx as nx
|
||||
from collections import defaultdict
|
||||
from itertools import product as iproduct
|
||||
from triangulation_gen import enumerate_all_triangulations
|
||||
from level_cycles import compute_levels, level_sources
|
||||
from depth_monovariant_check import (
|
||||
faces_of_subgraph, canonical_face, edges_of_face,
|
||||
apex_levels_for_edge, identify_outer_face, face_depths,
|
||||
)
|
||||
from full_resolution_check import (
|
||||
phase1_resolve_isolated, interior_odd_faces,
|
||||
)
|
||||
|
||||
|
||||
def break_source_triangle(G, source_set):
|
||||
"""If source is a face (3 vertices) and the source triangle is present
|
||||
in G, flip one of its edges. Returns modified G."""
|
||||
if len(source_set) != 3:
|
||||
return G
|
||||
a, b, c = tuple(source_set)
|
||||
if not (G.has_edge(a, b) and G.has_edge(b, c) and G.has_edge(a, c)):
|
||||
return G
|
||||
ip, emb = nx.check_planarity(G)
|
||||
if not ip:
|
||||
return G
|
||||
# Flip an edge of the source triangle.
|
||||
for (u, v) in [(a, b), (b, c), (a, c)]:
|
||||
f1 = emb.traverse_face(u, v)
|
||||
f2 = emb.traverse_face(v, u)
|
||||
if len(f1) != 3 or len(f2) != 3:
|
||||
continue
|
||||
w = next(x for x in f1 if x != u and x != v)
|
||||
x = next(y for y in f2 if y != u and y != v)
|
||||
if w == x or G.has_edge(w, x):
|
||||
continue
|
||||
Gp = G.copy()
|
||||
Gp.remove_edge(u, v)
|
||||
Gp.add_edge(w, x)
|
||||
return Gp
|
||||
return G
|
||||
|
||||
|
||||
def phase2_outer_incident_choices(G, emb, levels, nodes_k, k,
|
||||
include_even=False):
|
||||
"""For each simple cycle of L_k incident to the outer face, list available
|
||||
outer-face edge flips. Odd cycles: must flip (one choice required).
|
||||
Even cycles (if include_even): optional flip (None choice added)."""
|
||||
faces = faces_of_subgraph(G, emb, nodes_k)
|
||||
if not faces:
|
||||
return []
|
||||
outer = identify_outer_face(faces, G, emb, levels, k)
|
||||
if outer is None:
|
||||
return []
|
||||
outer_face = next((f for f in faces if canonical_face(f) == outer), None)
|
||||
if outer_face is None:
|
||||
return []
|
||||
outer_edges = set(frozenset(list(e)) for e in edges_of_face(outer_face))
|
||||
per_cycle = []
|
||||
for face in faces:
|
||||
cf = canonical_face(face)
|
||||
if cf == outer:
|
||||
continue
|
||||
is_odd = (len(face) % 2 == 1)
|
||||
if not is_odd and not include_even:
|
||||
continue
|
||||
face_edges = [frozenset([a, b]) for (a, b) in edges_of_face(face)]
|
||||
if not any(e in outer_edges for e in face_edges):
|
||||
continue
|
||||
choices = []
|
||||
for (u, v) in edges_of_face(face):
|
||||
if frozenset([u, v]) not in outer_edges:
|
||||
continue
|
||||
if not G.has_edge(u, v):
|
||||
continue
|
||||
result = apex_levels_for_edge(G, emb, u, v, levels)
|
||||
if result is None:
|
||||
continue
|
||||
_, (w, x) = result
|
||||
multi = G.has_edge(w, x)
|
||||
choices.append((multi, u, v, w, x))
|
||||
if not choices:
|
||||
continue
|
||||
choices.sort(key=lambda c: c[0])
|
||||
cycle_choices = [(c[1], c[2], c[3], c[4]) for c in choices]
|
||||
if not is_odd:
|
||||
# Even cycle: prepend None (skip)
|
||||
cycle_choices = [None] + cycle_choices
|
||||
per_cycle.append(cycle_choices)
|
||||
return per_cycle
|
||||
|
||||
|
||||
def apply_phase2_choices(G, choices):
|
||||
"""Apply a list of (u, v, w, x) flips in sequence; None entries are skips."""
|
||||
Gp = G.copy()
|
||||
applied = set()
|
||||
for choice in choices:
|
||||
if choice is None:
|
||||
continue
|
||||
u, v, w, x = choice
|
||||
if frozenset([u, v]) in applied:
|
||||
continue
|
||||
if Gp.has_edge(u, v):
|
||||
Gp.remove_edge(u, v)
|
||||
Gp.add_edge(w, x)
|
||||
applied.add(frozenset([u, v]))
|
||||
return Gp
|
||||
|
||||
|
||||
def interior_faces_any_parity(faces, depths, outer_canon, outer_face_edges):
|
||||
"""Interior faces (depth > 0, no outer-face edges) of any parity."""
|
||||
out = []
|
||||
for face in faces:
|
||||
cf = canonical_face(face)
|
||||
if cf == outer_canon:
|
||||
continue
|
||||
d = depths.get(cf)
|
||||
if d is None or d <= 0:
|
||||
continue
|
||||
oe = sum(1 for (a, b) in edges_of_face(face)
|
||||
if frozenset([a, b]) in outer_face_edges)
|
||||
if oe > 0:
|
||||
continue
|
||||
out.append((cf, face, d, len(face) % 2 == 1))
|
||||
return out
|
||||
|
||||
|
||||
def phase1_one_step_choices(G, emb, levels, nodes_k, k, include_even=True):
|
||||
"""Return all (u, v, w, x) flips Phase 1 might take in one step.
|
||||
If include_even, even interior faces are also targets, but only when
|
||||
no interior odd face is at maximum depth (we still resolve odd first)."""
|
||||
faces = faces_of_subgraph(G, emb, nodes_k)
|
||||
if len(faces) < 2:
|
||||
return []
|
||||
outer = identify_outer_face(faces, G, emb, levels, k)
|
||||
if outer is None:
|
||||
return []
|
||||
outer_face = next((f for f in faces if canonical_face(f) == outer), None)
|
||||
outer_face_edges = set()
|
||||
if outer_face is not None:
|
||||
for (a, b) in edges_of_face(outer_face):
|
||||
outer_face_edges.add(frozenset([a, b]))
|
||||
depths = face_depths(faces, outer)
|
||||
if depths is None:
|
||||
depths = {}
|
||||
interior_all = interior_faces_any_parity(faces, depths, outer,
|
||||
outer_face_edges)
|
||||
interior_odd = [(cf, f, d) for (cf, f, d, is_odd) in interior_all if is_odd]
|
||||
if not interior_odd:
|
||||
return [] # Phase 1 stops when no interior odd face remains
|
||||
max_d = max(d for _, _, d in interior_odd)
|
||||
# Deepest odd faces are the primary candidates.
|
||||
deepest = [(cf, f, d) for cf, f, d in interior_odd if d == max_d]
|
||||
# Additionally include ALL interior even faces (any depth >= 1) as
|
||||
# branching candidates — these are optional restructuring moves.
|
||||
if include_even:
|
||||
for (cf, f, d, is_odd) in interior_all:
|
||||
if not is_odd:
|
||||
deepest.append((cf, f, d))
|
||||
# For each deepest face, find its own lowest-depth-neighbor flip(s).
|
||||
all_flips = []
|
||||
for (cf_t, face_t, _) in deepest:
|
||||
face_canon = cf_t
|
||||
face_edges_set = set(frozenset([a, b])
|
||||
for (a, b) in edges_of_face(face_t))
|
||||
per_face = []
|
||||
for f in faces:
|
||||
cf = canonical_face(f)
|
||||
if cf == face_canon or cf == outer:
|
||||
continue
|
||||
shared = None
|
||||
for (a, b) in edges_of_face(f):
|
||||
if frozenset([a, b]) in face_edges_set:
|
||||
shared = (a, b)
|
||||
break
|
||||
if shared is None:
|
||||
continue
|
||||
d = depths.get(cf)
|
||||
if d is None:
|
||||
continue
|
||||
u, v = shared
|
||||
if not G.has_edge(u, v):
|
||||
continue
|
||||
result = apex_levels_for_edge(G, emb, u, v, levels)
|
||||
if result is None:
|
||||
continue
|
||||
_, (w, x) = result
|
||||
if G.has_edge(w, x):
|
||||
continue
|
||||
per_face.append((d, u, v, w, x))
|
||||
if not per_face:
|
||||
continue
|
||||
face_min_d = min(f[0] for f in per_face)
|
||||
for (d, u, v, w, x) in per_face:
|
||||
if d == face_min_d:
|
||||
all_flips.append((u, v, w, x))
|
||||
return all_flips
|
||||
|
||||
|
||||
def phase1_branch(G, levels, by_level, branch_limit=3, depth_limit=20):
|
||||
"""Branch on Phase 1 choices; yield all resulting graphs after Phase 1
|
||||
completes (or hits depth_limit)."""
|
||||
seen = set()
|
||||
def rec(Gc, step):
|
||||
sig = tuple(sorted(Gc.edges()))
|
||||
if sig in seen:
|
||||
return
|
||||
seen.add(sig)
|
||||
if step >= depth_limit:
|
||||
yield Gc
|
||||
return
|
||||
ip, emb = nx.check_planarity(Gc)
|
||||
if not ip:
|
||||
return
|
||||
# Find a level with phase 1 work to do
|
||||
for k in sorted(by_level):
|
||||
if k == 0:
|
||||
continue
|
||||
if len(by_level[k]) < 3:
|
||||
continue
|
||||
nodes_k = by_level[k]
|
||||
flips = phase1_one_step_choices(Gc, emb, levels, nodes_k, k)
|
||||
if flips:
|
||||
for (u, v, w, x) in flips[:branch_limit]:
|
||||
Gp = Gc.copy()
|
||||
Gp.remove_edge(u, v)
|
||||
Gp.add_edge(w, x)
|
||||
yield from rec(Gp, step + 1)
|
||||
return
|
||||
# No work to do anywhere
|
||||
yield Gc
|
||||
yield from rec(G, 0)
|
||||
|
||||
|
||||
def all_source_triangle_breaks(G, source_set):
|
||||
"""Yield (1) G unchanged, and (2..k) G with each source-triangle edge
|
||||
flipped (if applicable)."""
|
||||
yield G.copy()
|
||||
if len(source_set) != 3:
|
||||
return
|
||||
a, b, c = tuple(source_set)
|
||||
if not (G.has_edge(a, b) and G.has_edge(b, c) and G.has_edge(a, c)):
|
||||
return
|
||||
ip, emb = nx.check_planarity(G)
|
||||
if not ip:
|
||||
return
|
||||
for (u, v) in [(a, b), (b, c), (a, c)]:
|
||||
f1 = emb.traverse_face(u, v)
|
||||
f2 = emb.traverse_face(v, u)
|
||||
if len(f1) != 3 or len(f2) != 3:
|
||||
continue
|
||||
w = next(x for x in f1 if x != u and x != v)
|
||||
x = next(y for y in f2 if y != u and y != v)
|
||||
if w == x or G.has_edge(w, x):
|
||||
continue
|
||||
Gp = G.copy()
|
||||
Gp.remove_edge(u, v)
|
||||
Gp.add_edge(w, x)
|
||||
yield Gp
|
||||
|
||||
|
||||
def simple_level_resolutions(G, source_set, p1_branch=4, p2_branch=5):
|
||||
"""Enumerate Phase 1 + Phase 2 outputs with branching, trying all
|
||||
source-triangle-break options (including skipping the break)."""
|
||||
levels = compute_levels(G, source_set)
|
||||
by_level = defaultdict(list)
|
||||
for v, lv in levels.items():
|
||||
by_level[lv].append(v)
|
||||
seen_outputs = set()
|
||||
for Gc in all_source_triangle_breaks(G, source_set):
|
||||
# Branch on Phase 1
|
||||
for G_p1 in phase1_branch(Gc, levels, by_level,
|
||||
branch_limit=p1_branch):
|
||||
per_level_choices = []
|
||||
for k in sorted(by_level):
|
||||
if k == 0:
|
||||
continue
|
||||
if len(by_level[k]) < 3:
|
||||
continue
|
||||
nodes_k = by_level[k]
|
||||
ip, emb = nx.check_planarity(G_p1)
|
||||
if not ip:
|
||||
continue
|
||||
pcs = phase2_outer_incident_choices(G_p1, emb, levels,
|
||||
nodes_k, k,
|
||||
include_even=True)
|
||||
pcs = [c[:p2_branch] for c in pcs]
|
||||
per_level_choices.extend(pcs)
|
||||
if not per_level_choices:
|
||||
sig = tuple(sorted(G_p1.edges()))
|
||||
if sig not in seen_outputs:
|
||||
seen_outputs.add(sig)
|
||||
yield G_p1
|
||||
continue
|
||||
total = 1
|
||||
for c in per_level_choices:
|
||||
total *= len(c)
|
||||
if total > 4096:
|
||||
choices = [c[0] for c in per_level_choices]
|
||||
Gp = apply_phase2_choices(G_p1, choices)
|
||||
sig = tuple(sorted(Gp.edges()))
|
||||
if sig not in seen_outputs:
|
||||
seen_outputs.add(sig)
|
||||
yield Gp
|
||||
continue
|
||||
for combo in iproduct(*per_level_choices):
|
||||
Gp = apply_phase2_choices(G_p1, combo)
|
||||
sig = tuple(sorted(Gp.edges()))
|
||||
if sig not in seen_outputs:
|
||||
seen_outputs.add(sig)
|
||||
yield Gp
|
||||
|
||||
|
||||
def simple_level_resolve(G, source_set):
|
||||
"""Single-output version: yield the first triangulation output (if any),
|
||||
else fall back to first output."""
|
||||
first = None
|
||||
for Gp in simple_level_resolutions(G, source_set):
|
||||
if first is None:
|
||||
first = Gp
|
||||
if is_triangulation(Gp, G.number_of_nodes()):
|
||||
return Gp, 'ok'
|
||||
if first is None:
|
||||
return G.copy(), 'no_output'
|
||||
return first, 'ok'
|
||||
|
||||
|
||||
def is_triangulation(G, n):
|
||||
if not isinstance(G, nx.Graph):
|
||||
return False
|
||||
if G.number_of_nodes() != n:
|
||||
return False
|
||||
if G.number_of_edges() != 3 * n - 6:
|
||||
return False
|
||||
ip, _ = nx.check_planarity(G)
|
||||
return ip
|
||||
|
||||
|
||||
def iso_index(G, isos):
|
||||
for i, T in enumerate(isos):
|
||||
if nx.is_isomorphic(G, T):
|
||||
return i
|
||||
return None
|
||||
|
||||
|
||||
def is_self_level_resolution(T):
|
||||
"""Does T admit some level source S such that T is a level resolution
|
||||
of (T, S)? I.e., the parity subgraphs of T (under T's own BFS from S)
|
||||
are both bipartite."""
|
||||
ip, emb = nx.check_planarity(T)
|
||||
if not ip:
|
||||
return False
|
||||
for kind, label, source_set in level_sources(T, emb):
|
||||
levels = compute_levels(T, source_set)
|
||||
even = [v for v in T.nodes() if levels.get(v, 0) % 2 == 0]
|
||||
odd = [v for v in T.nodes() if levels.get(v, 0) % 2 == 1]
|
||||
if nx.is_bipartite(T.subgraph(even)) and \
|
||||
nx.is_bipartite(T.subgraph(odd)):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def run(n):
|
||||
isos = enumerate_all_triangulations(n)
|
||||
print(f"n={n}: {len(isos)} iso-classes")
|
||||
md4 = set()
|
||||
for i, T in enumerate(isos):
|
||||
if min(T.degree(v) for v in T.nodes()) >= 4:
|
||||
md4.add(i)
|
||||
print(f" min-degree-4 iso-classes: {len(md4)}")
|
||||
self_resolutions = set()
|
||||
for i, T in enumerate(isos):
|
||||
if is_self_level_resolution(T):
|
||||
self_resolutions.add(i)
|
||||
print(f" self-level-resolutions: {len(self_resolutions)} iso-classes")
|
||||
reached_any = set() # iso-class is the output (any triangulation)
|
||||
reached_bip = set() # iso-class is an output with bipartite parity
|
||||
total_pairs = 0
|
||||
triangulation_outputs = 0
|
||||
bipartite_outputs = 0
|
||||
for src_idx, G in enumerate(isos):
|
||||
ip, emb = nx.check_planarity(G)
|
||||
if not ip:
|
||||
continue
|
||||
for kind, label, source_set in level_sources(G, emb):
|
||||
total_pairs += 1
|
||||
for Gp in simple_level_resolutions(G, source_set):
|
||||
if not is_triangulation(Gp, n):
|
||||
continue
|
||||
triangulation_outputs += 1
|
||||
levels = compute_levels(G, source_set)
|
||||
even_nodes = [v for v in Gp.nodes()
|
||||
if levels.get(v, 0) % 2 == 0]
|
||||
odd_nodes = [v for v in Gp.nodes()
|
||||
if levels.get(v, 0) % 2 == 1]
|
||||
is_bip = (nx.is_bipartite(Gp.subgraph(even_nodes)) and
|
||||
nx.is_bipartite(Gp.subgraph(odd_nodes)))
|
||||
if is_bip:
|
||||
bipartite_outputs += 1
|
||||
tgt = iso_index(Gp, isos)
|
||||
if tgt is not None:
|
||||
reached_any.add(tgt)
|
||||
if is_bip:
|
||||
reached_bip.add(tgt)
|
||||
# Refined conjecture: every min-degree-4 iso-class is reached.
|
||||
needed = md4 - self_resolutions
|
||||
missing_any = sorted(needed - reached_any)
|
||||
missing_bip = sorted(needed - reached_bip)
|
||||
print(f" (G, S) pairs: {total_pairs}")
|
||||
print(f" triangulation outputs: {triangulation_outputs}")
|
||||
print(f" bipartite-parity: {bipartite_outputs}")
|
||||
print(f" needed (md4, not self): {len(needed)} iso-classes")
|
||||
print(f" reached as any output: {len(reached_any & needed)} / {len(needed)}"
|
||||
f" (missing: {missing_any[:10] if missing_any else 'none'}"
|
||||
f"{'...' if len(missing_any) > 10 else ''})")
|
||||
print(f" reached as bip-parity: {len(reached_bip & needed)} / {len(needed)}"
|
||||
f" (missing: {missing_bip[:10] if missing_bip else 'none'}"
|
||||
f"{'...' if len(missing_bip) > 10 else ''})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1:
|
||||
ns = [int(x) for x in sys.argv[1:]]
|
||||
else:
|
||||
ns = [6, 7, 8]
|
||||
for n in ns:
|
||||
run(n)
|
||||
@@ -0,0 +1,134 @@
|
||||
"""Visualize the Phase 2 failure case: n=10, tri[13], face source (1,2,4),
|
||||
L_1. Highlights the interior odd triangle (0,5,6) that the algorithm misses,
|
||||
and labels each edge by its (apex, apex) level pair."""
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import networkx as nx
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
from collections import defaultdict
|
||||
from triangulation_gen import enumerate_all_triangulations
|
||||
from level_cycles import compute_levels
|
||||
from depth_monovariant_check import (
|
||||
faces_of_subgraph, canonical_face, edges_of_face,
|
||||
apex_levels_for_edge, identify_outer_face,
|
||||
)
|
||||
from viz_cycling import layout_outerplanar
|
||||
|
||||
|
||||
def main():
|
||||
tris = enumerate_all_triangulations(10)
|
||||
G = tris[13]
|
||||
ip, emb = nx.check_planarity(G)
|
||||
source = {1, 2, 4}
|
||||
levels = compute_levels(G, source)
|
||||
by_level = defaultdict(list)
|
||||
for v, lv in levels.items():
|
||||
by_level[lv].append(v)
|
||||
nodes_k = by_level[1]
|
||||
k = 1
|
||||
|
||||
faces = faces_of_subgraph(G, emb, nodes_k)
|
||||
outer = identify_outer_face(faces, G, emb, levels, k)
|
||||
outer_face = next(f for f in faces if canonical_face(f) == outer)
|
||||
outer_edges = set(frozenset(list(e)) for e in edges_of_face(outer_face))
|
||||
|
||||
pos = layout_outerplanar(nodes_k, outer_face, G)
|
||||
fig, ax = plt.subplots(1, 1, figsize=(10, 10))
|
||||
|
||||
# Shade odd faces
|
||||
for f in faces:
|
||||
cf = canonical_face(f)
|
||||
if cf == outer:
|
||||
continue
|
||||
if len(f) % 2 != 1:
|
||||
continue
|
||||
incident = any(frozenset([a, b]) in outer_edges
|
||||
for (a, b) in edges_of_face(f))
|
||||
if incident:
|
||||
color = '#ffd45a' # outer-incident odd (Phase 2 targets)
|
||||
label = "Phase 2 flips here"
|
||||
alpha = 0.65
|
||||
else:
|
||||
color = '#ff5555' # interior odd (algorithm misses)
|
||||
label = "MISSED"
|
||||
alpha = 0.7
|
||||
poly = plt.Polygon([pos[v] for v in f], color=color, alpha=alpha,
|
||||
zorder=1)
|
||||
ax.add_patch(poly)
|
||||
cx, cy = np.mean([pos[v] for v in f], axis=0)
|
||||
ax.text(cx, cy, f"{tuple(f)}\nlen={len(f)} (odd)\n{label}",
|
||||
fontsize=9, ha='center', va='center', zorder=3,
|
||||
fontweight='bold')
|
||||
|
||||
# Edges with apex-pair labels
|
||||
Lk = G.subgraph(nodes_k)
|
||||
for u, v in Lk.edges():
|
||||
ax.plot([pos[u][0], pos[v][0]], [pos[u][1], pos[v][1]],
|
||||
color='black', linewidth=1.6, zorder=2)
|
||||
result = apex_levels_for_edge(G, emb, u, v, levels)
|
||||
if result is None:
|
||||
continue
|
||||
(la, lb), (w, x) = result
|
||||
mid = (pos[u] + pos[v]) / 2
|
||||
# Apex pair relative to k
|
||||
pair = tuple(sorted([la - k, lb - k]))
|
||||
if pair == (0, 0):
|
||||
apex_color = '#cc0000' # intra-level (k,k)
|
||||
apex_label = '(k,k)'
|
||||
elif pair == (-1, 0) or pair == (-1, 1):
|
||||
apex_color = '#006400' # cross-parity / free
|
||||
apex_label = f'(k{la-k:+d},k{lb-k:+d})'
|
||||
else:
|
||||
apex_color = '#0044aa'
|
||||
apex_label = f'(k{la-k:+d},k{lb-k:+d})'
|
||||
ax.text(mid[0], mid[1], apex_label, fontsize=7,
|
||||
ha='center', va='center', color=apex_color,
|
||||
bbox=dict(facecolor='white', edgecolor='none', alpha=0.85,
|
||||
pad=1.0), zorder=4)
|
||||
|
||||
for v in nodes_k:
|
||||
ax.plot(pos[v][0], pos[v][1], 'o', color='black', markersize=14,
|
||||
zorder=4)
|
||||
ax.text(pos[v][0] * 1.13, pos[v][1] * 1.13, str(v), fontsize=16,
|
||||
ha='center', va='center', color='blue', zorder=5,
|
||||
fontweight='bold')
|
||||
|
||||
# Highlight the cross-level edge of (0,5,6) that would resolve it.
|
||||
# Find the (k,k+1) or (k,k-1) or (k-1,k+1) edge of triangle (0,5,6).
|
||||
interior_face = (0, 5, 6)
|
||||
for (u, v) in edges_of_face(interior_face):
|
||||
if not Lk.has_edge(u, v):
|
||||
continue
|
||||
result = apex_levels_for_edge(G, emb, u, v, levels)
|
||||
if result is None:
|
||||
continue
|
||||
(la, lb), _ = result
|
||||
pair = tuple(sorted([la - k, lb - k]))
|
||||
if pair != (0, 0):
|
||||
ax.plot([pos[u][0], pos[v][0]], [pos[u][1], pos[v][1]],
|
||||
color='green', linewidth=4.0, zorder=2.5, linestyle=':')
|
||||
break
|
||||
|
||||
ax.set_aspect('equal')
|
||||
ax.axis('off')
|
||||
ax.set_xlim(-1.3, 1.3)
|
||||
ax.set_ylim(-1.4, 1.3)
|
||||
title = (
|
||||
"Phase 2 gap: n=10, tri[13], source face (1,2,4), $L_1$\n"
|
||||
"Yellow = outer-incident odd faces (Phase 2 flips one edge each).\n"
|
||||
"Red = interior odd face (0,5,6) — Phase 2 misses, Phase 1 ignores "
|
||||
"(not tricky-everywhere — its (0,6) edge has apex (k,k+1)).\n"
|
||||
"Green dotted: the cross-level edge (0,6) whose flip would resolve "
|
||||
"(0,5,6) but is unreached by either phase."
|
||||
)
|
||||
fig.suptitle(title, fontsize=12)
|
||||
plt.tight_layout(rect=[0, 0, 1, 0.90])
|
||||
out = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
'..', 'phase2_gap_visualization.png')
|
||||
plt.savefig(out, dpi=130, bbox_inches='tight')
|
||||
print(f"saved: {out}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -43,15 +43,19 @@
|
||||
\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{6.1}{Apex classification of $L_k$-edges}}{5}{subsection.6.1}\protected@file@percent }
|
||||
\newlabel{lem:bridge-apex}{{6.1}{5}{}{theorem.6.1}{}}
|
||||
\newlabel{prop:flip-target}{{6.2}{5}{}{theorem.6.2}{}}
|
||||
\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{6.2}{Cross-level flip pass}}{5}{subsection.6.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{6.3}{Tricky-everywhere cycles}}{5}{subsection.6.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{6.2}{Facial depth and isolated faces}}{5}{subsection.6.2}\protected@file@percent }
|
||||
\newlabel{def:facial-depth}{{6.3}{5}{Facial depth}{theorem.6.3}{}}
|
||||
\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{6.4}{The algorithm}}{6}{subsection.6.4}\protected@file@percent }
|
||||
\newlabel{obs:terminate}{{6.4}{6}{}{theorem.6.4}{}}
|
||||
\newlabel{q:terminate-all-n}{{6.5}{6}{}{theorem.6.5}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\tocsection {}{7}{Discussion and open questions}}{6}{section.7}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\tocsection {}{8}{Implementation}}{6}{section.8}\protected@file@percent }
|
||||
\newlabel{sec:impl}{{8}{6}{Implementation}{section.8}{}}
|
||||
\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{6.3}{Phase 1: interior faces}}{5}{subsection.6.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{6.4}{Phase 2: outer-incident faces}}{6}{subsection.6.4}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{6.5}{Simple level resolutions}}{6}{subsection.6.5}\protected@file@percent }
|
||||
\newlabel{def:simple-level-resolution}{{6.4}{6}{Simple level resolution}{theorem.6.4}{}}
|
||||
\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{6.6}{Empirical status}}{6}{subsection.6.6}\protected@file@percent }
|
||||
\newlabel{obs:empirical-lk-bipartite}{{6.5}{6}{}{theorem.6.5}{}}
|
||||
\@writefile{toc}{\contentsline {paragraph}{\tocparagraph {}{}{Coverage test for Conjecture\nonbreakingspace \ref {conj:simple-md4}.}}{6}{section*.2}\protected@file@percent }
|
||||
\newlabel{obs:md4-simple-resolution}{{6.6}{7}{}{theorem.6.6}{}}
|
||||
\newlabel{conj:simple-md4}{{6.7}{7}{Simple-resolution $\mathrm {md}_4$ surjectivity}{theorem.6.7}{}}
|
||||
\newlabel{q:terminate-all-n}{{6.8}{7}{}{theorem.6.8}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\tocsection {}{7}{Discussion and open questions}}{7}{section.7}\protected@file@percent }
|
||||
\bibcite{appelhaken}{1}
|
||||
\bibcite{rsst}{2}
|
||||
\bibcite{tutte}{3}
|
||||
@@ -61,5 +65,7 @@
|
||||
\newlabel{tocindent1}{17.77782pt}
|
||||
\newlabel{tocindent2}{29.38873pt}
|
||||
\newlabel{tocindent3}{0pt}
|
||||
\@writefile{toc}{\contentsline {section}{\tocsection {}{}{References}}{7}{section*.2}\protected@file@percent }
|
||||
\gdef \@abspage@last{7}
|
||||
\@writefile{toc}{\contentsline {section}{\tocsection {}{8}{Implementation}}{8}{section.8}\protected@file@percent }
|
||||
\newlabel{sec:impl}{{8}{8}{Implementation}{section.8}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\tocsection {}{}{References}}{8}{section*.3}\protected@file@percent }
|
||||
\gdef \@abspage@last{8}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 20 MAY 2026 01:17
|
||||
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 20 MAY 2026 13:40
|
||||
entering extended mode
|
||||
restricted \write18 enabled.
|
||||
%&-line parsing enabled.
|
||||
@@ -363,18 +363,27 @@ Package hyperref Warning: Token not allowed in a PDF string (Unicode):
|
||||
Package hyperref Warning: Token not allowed in a PDF string (Unicode):
|
||||
(hyperref) removing `math shift' on input line 370.
|
||||
|
||||
[5] [6] [7] (./paper.aux)
|
||||
Package rerunfilecheck Info: File `paper.out' has not changed.
|
||||
(rerunfilecheck) Checksum: C4EC0643C07D7F8B157295F889BACC80;3432.
|
||||
[5] [6] [7] [8] (./paper.aux)
|
||||
|
||||
LaTeX Warning: Label(s) may have changed. Rerun to get cross-references right.
|
||||
|
||||
|
||||
Package rerunfilecheck Warning: File `paper.out' has changed.
|
||||
(rerunfilecheck) Rerun to get outlines right
|
||||
(rerunfilecheck) or use package `bookmark'.
|
||||
|
||||
Package rerunfilecheck Info: Checksums for `paper.out':
|
||||
(rerunfilecheck) Before: 00D0EBD21A3E3804EF6FF0D2A45466A8;3956
|
||||
(rerunfilecheck) After: C580028718693FDA81B49B22CE461AD9;3956.
|
||||
)
|
||||
Here is how much of TeX's memory you used:
|
||||
8935 strings out of 478268
|
||||
137960 string characters out of 5846347
|
||||
440651 words of memory out of 5000000
|
||||
26851 multiletter control sequences out of 15000+600000
|
||||
8945 strings out of 478268
|
||||
138142 string characters out of 5846347
|
||||
440795 words of memory out of 5000000
|
||||
26856 multiletter control sequences out of 15000+600000
|
||||
475834 words of font info for 54 fonts, out of 8000000 for 9000
|
||||
1302 hyphenation exceptions out of 8191
|
||||
69i,9n,76p,396b,463s stack positions out of 10000i,1000n,20000p,200000b,200000s
|
||||
69i,9n,76p,396b,466s stack positions out of 10000i,1000n,20000p,200000b,200000s
|
||||
</usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx10.pfb
|
||||
></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmcsc10.pfb
|
||||
></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi10.pfb>
|
||||
@@ -393,10 +402,10 @@ ve/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy6.pfb></usr/local/texlive
|
||||
/texmf-dist/fonts/type1/public/amsfonts/cm/cmtt10.pfb></usr/local/texlive/2022/
|
||||
texmf-dist/fonts/type1/public/amsfonts/symbols/msam10.pfb></usr/local/texlive/2
|
||||
022/texmf-dist/fonts/type1/public/amsfonts/symbols/msbm10.pfb>
|
||||
Output written on paper.pdf (7 pages, 254719 bytes).
|
||||
Output written on paper.pdf (8 pages, 258447 bytes).
|
||||
PDF statistics:
|
||||
264 PDF objects out of 1000 (max. 8388607)
|
||||
216 compressed objects within 3 object streams
|
||||
55 named destinations out of 1000 (max. 500000)
|
||||
129 words of extra memory for PDF output out of 10000 (max. 10000000)
|
||||
281 PDF objects out of 1000 (max. 8388607)
|
||||
232 compressed objects within 3 object streams
|
||||
60 named destinations out of 1000 (max. 500000)
|
||||
145 words of extra memory for PDF output out of 10000 (max. 10000000)
|
||||
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
\BOOKMARK [2][-]{subsection.5.3}{\376\377\0005\000.\0003\000.\000\040\000R\000e\000s\000t\000a\000t\000e\000m\000e\000n\000t\000\040\000o\000f\000\040\000t\000h\000e\000\040\000r\000e\000s\000o\000l\000u\000t\000i\000o\000n\000-\000p\000r\000e\000i\000m\000a\000g\000e\000\040\000c\000o\000n\000j\000e\000c\000t\000u\000r\000e}{section.5}% 8
|
||||
\BOOKMARK [1][-]{section.6}{\376\377\0006\000.\000\040\000A\000n\000\040\000e\000d\000g\000e\000-\000f\000l\000i\000p\000\040\000r\000e\000s\000o\000l\000u\000t\000i\000o\000n\000\040\000a\000l\000g\000o\000r\000i\000t\000h\000m}{}% 9
|
||||
\BOOKMARK [2][-]{subsection.6.1}{\376\377\0006\000.\0001\000.\000\040\000A\000p\000e\000x\000\040\000c\000l\000a\000s\000s\000i\000f\000i\000c\000a\000t\000i\000o\000n\000\040\000o\000f\000\040\000L\000k\000-\000e\000d\000g\000e\000s}{section.6}% 10
|
||||
\BOOKMARK [2][-]{subsection.6.2}{\376\377\0006\000.\0002\000.\000\040\000C\000r\000o\000s\000s\000-\000l\000e\000v\000e\000l\000\040\000f\000l\000i\000p\000\040\000p\000a\000s\000s}{section.6}% 11
|
||||
\BOOKMARK [2][-]{subsection.6.3}{\376\377\0006\000.\0003\000.\000\040\000T\000r\000i\000c\000k\000y\000-\000e\000v\000e\000r\000y\000w\000h\000e\000r\000e\000\040\000c\000y\000c\000l\000e\000s}{section.6}% 12
|
||||
\BOOKMARK [2][-]{subsection.6.4}{\376\377\0006\000.\0004\000.\000\040\000T\000h\000e\000\040\000a\000l\000g\000o\000r\000i\000t\000h\000m}{section.6}% 13
|
||||
\BOOKMARK [1][-]{section.7}{\376\377\0007\000.\000\040\000D\000i\000s\000c\000u\000s\000s\000i\000o\000n\000\040\000a\000n\000d\000\040\000o\000p\000e\000n\000\040\000q\000u\000e\000s\000t\000i\000o\000n\000s}{}% 14
|
||||
\BOOKMARK [1][-]{section.8}{\376\377\0008\000.\000\040\000I\000m\000p\000l\000e\000m\000e\000n\000t\000a\000t\000i\000o\000n}{}% 15
|
||||
\BOOKMARK [1][-]{section*.2}{\376\377\000R\000e\000f\000e\000r\000e\000n\000c\000e\000s}{}% 16
|
||||
\BOOKMARK [2][-]{subsection.6.2}{\376\377\0006\000.\0002\000.\000\040\000F\000a\000c\000i\000a\000l\000\040\000d\000e\000p\000t\000h\000\040\000a\000n\000d\000\040\000i\000s\000o\000l\000a\000t\000e\000d\000\040\000f\000a\000c\000e\000s}{section.6}% 11
|
||||
\BOOKMARK [2][-]{subsection.6.3}{\376\377\0006\000.\0003\000.\000\040\000P\000h\000a\000s\000e\000\040\0001\000:\000\040\000i\000n\000t\000e\000r\000i\000o\000r\000\040\000f\000a\000c\000e\000s}{section.6}% 12
|
||||
\BOOKMARK [2][-]{subsection.6.4}{\376\377\0006\000.\0004\000.\000\040\000P\000h\000a\000s\000e\000\040\0002\000:\000\040\000o\000u\000t\000e\000r\000-\000i\000n\000c\000i\000d\000e\000n\000t\000\040\000f\000a\000c\000e\000s}{section.6}% 13
|
||||
\BOOKMARK [2][-]{subsection.6.5}{\376\377\0006\000.\0005\000.\000\040\000S\000i\000m\000p\000l\000e\000\040\000l\000e\000v\000e\000l\000\040\000r\000e\000s\000o\000l\000u\000t\000i\000o\000n\000s}{section.6}% 14
|
||||
\BOOKMARK [2][-]{subsection.6.6}{\376\377\0006\000.\0006\000.\000\040\000E\000m\000p\000i\000r\000i\000c\000a\000l\000\040\000s\000t\000a\000t\000u\000s}{section.6}% 15
|
||||
\BOOKMARK [1][-]{section.7}{\376\377\0007\000.\000\040\000D\000i\000s\000c\000u\000s\000s\000i\000o\000n\000\040\000a\000n\000d\000\040\000o\000p\000e\000n\000\040\000q\000u\000e\000s\000t\000i\000o\000n\000s}{}% 16
|
||||
\BOOKMARK [1][-]{section.8}{\376\377\0008\000.\000\040\000I\000m\000p\000l\000e\000m\000e\000n\000t\000a\000t\000i\000o\000n}{}% 17
|
||||
\BOOKMARK [1][-]{section*.3}{\376\377\000R\000e\000f\000e\000r\000e\000n\000c\000e\000s}{}% 18
|
||||
|
||||
Binary file not shown.
@@ -399,25 +399,7 @@ $L_{k+1}$ iff $\ell_G(w) = \ell_G(x) = k + 1$; otherwise $wx$ is cross-parity
|
||||
and lies in no level subgraph. In all cases $uv$ is removed from $L_k$.
|
||||
\end{proposition}
|
||||
|
||||
\subsection{Cross-level flip pass}
|
||||
|
||||
For each odd simple cycle $C$ of each $L_k$ containing a cross-level edge,
|
||||
flip one such edge. By Proposition~\ref{prop:flip-target} the new edge
|
||||
either enters $L_{k+1}$ (in the apex case $(k+1, k+1)$) or is cross-parity
|
||||
(otherwise). Choosing apex pairs distinctly across cycles makes the set of
|
||||
new edges entering any single level $L_j$ a matching, hence a forest, and
|
||||
similarly for new same-parity-distance-$2$ edges entering the relevant
|
||||
parity subgraph; these therefore introduce no odd cycle.
|
||||
|
||||
\subsection{Tricky-everywhere cycles}
|
||||
|
||||
After the cross-level pass, the only odd simple cycles remaining in any
|
||||
$L_k$ are those whose every edge is intra-level; we call such a cycle
|
||||
\emph{tricky-everywhere}. By Proposition~\ref{prop:flip-target}, flipping
|
||||
any edge of a tricky-everywhere cycle replaces it with another edge of $L_k$,
|
||||
so the local triangle pair $(uvw, uvx)$ becomes $(uwx, vwx)$: still a pair
|
||||
of odd triangles inside $L_k$. To make global progress on these cycles we
|
||||
use a facial-depth potential to choose the flip.
|
||||
\subsection{Facial depth and isolated faces}
|
||||
|
||||
\begin{definition}[Facial depth]
|
||||
\label{def:facial-depth}
|
||||
@@ -430,41 +412,138 @@ $L_k$ is
|
||||
\mathrm{depth}(F) \;=\; \min_{F' \in \mathcal{B}} \mathrm{dist}_D(F, F'),
|
||||
\]
|
||||
with the convention $\mathrm{depth}(F) = \infty$ if no such $F'$ exists.
|
||||
An inner face is \emph{isolated} if $\mathrm{depth}(F) \geq 1$.
|
||||
\end{definition}
|
||||
|
||||
\subsection{The algorithm}
|
||||
The seed set $\mathcal{B}$ consists of the inner faces that already have
|
||||
two outer-face edges available as flip targets; Phase~2 below handles
|
||||
these directly. Phase~1 uses facial depth as a potential to push isolated
|
||||
odd faces toward $\mathcal{B}$.
|
||||
|
||||
\begin{enumerate}
|
||||
\item \emph{Cross-level flip pass.} For each $L_k$ and each odd simple cycle
|
||||
$C \subseteq L_k$ containing a cross-level edge, flip one such edge,
|
||||
selecting apex pairs to keep the newly added edges a matching in each
|
||||
target level subgraph and in the relevant parity subgraph.
|
||||
\item \emph{Intra-level flip loop.} While some $L_k$ contains a
|
||||
tricky-everywhere odd simple cycle:
|
||||
\begin{enumerate}
|
||||
A cycle that is \emph{tricky-everywhere}, meaning every edge is
|
||||
intra-level, is necessarily isolated: an outer-face edge of $L_k$ has a
|
||||
level-$(k-1)$ apex on its outer side and is therefore cross-level, so a
|
||||
tricky-everywhere cycle shares no edge with the outer face. Hence the
|
||||
tricky-everywhere cycles are a subset of the isolated odd cycles.
|
||||
|
||||
\subsection{Phase 1: interior faces}
|
||||
|
||||
\noindent\textbf{Procedure.} While some $L_k$ contains an odd simple cycle
|
||||
whose corresponding inner face has facial depth $\geq 1$ and shares no
|
||||
edge with the outer face, repeat:
|
||||
\begin{enumerate}[label=(\arabic*)]
|
||||
\item compute facial depths for all simple level cycles of $L_k$;
|
||||
\item among tricky-everywhere odd simple cycles of maximum facial depth,
|
||||
pick any $C$;
|
||||
\item among the edges of $C$, pick one whose other incident inner face has
|
||||
minimum facial depth, and flip it.
|
||||
\end{enumerate}
|
||||
\item among interior odd faces (depth $\geq 1$, no outer-face edges) of
|
||||
maximum facial depth, pick one $C$; even-parity interior faces of
|
||||
depth $\geq 1$ may also be selected as $C$;
|
||||
\item find the inner face $F'$ incident to $C$ of minimum facial depth,
|
||||
and flip the edge shared between $C$ and $F'$.
|
||||
\end{enumerate}
|
||||
|
||||
The restriction to faces with no outer-face edge in step~(2) means that
|
||||
every edge of $C$ borders another inner face, so a unique shared-edge
|
||||
flip target exists for each neighbor $F'$. The depth-guided choice of
|
||||
$F'$ in step~(3) progressively pushes the residual odd-face structure
|
||||
toward the seed set $\mathcal{B}$ (depth~$0$). Even-face flips are
|
||||
optional restructuring moves that expand the reachable configuration
|
||||
space; the loop's termination is gated only by interior odd faces.
|
||||
|
||||
\subsection{Phase 2: outer-incident faces}
|
||||
|
||||
After Phase~1, every remaining odd simple cycle of $L_k$ shares at least
|
||||
one edge with the outer face, whose apex pair includes a level-$(k-1)$
|
||||
vertex and is therefore cross-level.
|
||||
|
||||
\noindent\textbf{Procedure.}
|
||||
For each $L_k$:
|
||||
\begin{itemize}
|
||||
\item every odd simple cycle $C \subseteq L_k$ incident to the outer
|
||||
face \emph{must} have exactly one of its outer-face edges flipped;
|
||||
\item every even simple cycle of $L_k$ incident to the outer face
|
||||
\emph{may} have at most one of its outer-face edges flipped (an optional
|
||||
restructuring move).
|
||||
\end{itemize}
|
||||
For the source-face level ($k = 0$ with face source $S$), the $L_0$
|
||||
source triangle is itself an odd cycle whose three edges all bound the
|
||||
outer face; we treat $L_0$ uniformly with higher levels, with the option
|
||||
of leaving the triangle intact when the resulting parity-subgraph
|
||||
configuration on $G'$ permits.
|
||||
|
||||
Each flip is permitted even if the apex edge $wx$ already exists in $G$,
|
||||
in which case $G'$ is a multigraph; this does not affect bipartiteness
|
||||
of the parity subgraphs of $G'$, since a duplicated edge is bipartite-
|
||||
equivalent to a single edge.
|
||||
|
||||
\subsection{Simple level resolutions}
|
||||
|
||||
\begin{definition}[Simple level resolution]
|
||||
\label{def:simple-level-resolution}
|
||||
A plane triangulation $G'$ is a \emph{simple level resolution} of a plane
|
||||
triangulation $G$ if there exists a level source $S$ of $G$ such that
|
||||
the algorithm of Sections~\ref{sec:flip-algorithm}--Phase~1 and~Phase~2
|
||||
applied to $(G, S)$, under some sequence of optional-move choices,
|
||||
produces $G'$ as a simple-graph triangulation whose parity subgraphs are
|
||||
bipartite.
|
||||
\end{definition}
|
||||
|
||||
\subsection{Empirical status}
|
||||
|
||||
\begin{observation}
|
||||
\label{obs:terminate}
|
||||
\label{obs:empirical-lk-bipartite}
|
||||
For every plane triangulation $G$ on $n \in \{9, 10, 11\}$ vertices, every
|
||||
level source $S$, and every $k$ such that $L_k$ contains a tricky-everywhere
|
||||
odd simple cycle, Step~2 terminates with no tricky-everywhere odd simple
|
||||
cycle remaining in any $L_k$. Moreover, the total number of
|
||||
tricky-everywhere odd simple cycles strictly decreases on every flip chosen
|
||||
by Step~2(c).
|
||||
level source $S$, and every $k$ such that $L_k$ contains an odd simple
|
||||
cycle, the algorithm produces a $G'$ whose corresponding $L_k$ is
|
||||
bipartite (in the underlying simple-graph view). Across the $29640$ such
|
||||
$(G, S, k)$ triples — $4645$ at $n \leq 10$ and $24995$ at $n = 11$ —
|
||||
Phase~1 always terminates and Phase~2 always succeeds.
|
||||
\end{observation}
|
||||
|
||||
\paragraph{Coverage test for Conjecture~\ref{conj:simple-md4}.} For each
|
||||
$n \in \{6, \ldots, 11\}$ we enumerate all plane-triangulation iso-classes
|
||||
on $n$ vertices. For each iso-class $G$, each level source $S$ of $G$,
|
||||
and each branching choice within the algorithm — Phase~1 ties on which
|
||||
deepest interior face and which lowest-depth neighbor to flip, Phase~2
|
||||
choices of which outer-face edge to flip for each odd or even
|
||||
outer-incident cycle (including the option to leave even cycles
|
||||
untouched), and the option to skip the source-triangle break when $S$ is
|
||||
a face — we run Phase~1 to termination and then Phase~2, recording the
|
||||
algorithm's output $G'$ as a labelled simple graph. We check three
|
||||
properties: (a) $G'$ is a triangulation (no multi-edge survived the
|
||||
Phase~2 flips), (b) the parity subgraphs $E_{G, S}(G')$ and
|
||||
$O_{G, S}(G')$ are both bipartite, and (c) the iso-class of $G'$.
|
||||
Aggregating over all $(G, S, \text{branch-choices})$ triples yields the
|
||||
set of iso-classes attainable as algorithm outputs satisfying (a)+(b);
|
||||
we compare this set against the minimum-degree-$4$ iso-classes at each
|
||||
$n$.
|
||||
|
||||
\begin{observation}
|
||||
\label{obs:md4-simple-resolution}
|
||||
For every $n \in \{6, 7, 8, 9, 10, 11\}$, every plane triangulation
|
||||
iso-class on $n$ vertices with minimum degree at least $4$ is a simple
|
||||
level resolution of some plane triangulation on $n$ vertices. Concretely,
|
||||
the counts of minimum-degree-$4$ iso-classes — $1, 1, 2, 5, 12, 34$ at
|
||||
$n = 6, \ldots, 11$ — are all reached by the algorithm with bipartite
|
||||
parity subgraphs (Definition~\ref{def:simple-level-resolution}).
|
||||
\end{observation}
|
||||
|
||||
\begin{conjecture}[Simple-resolution $\mathrm{md}_4$ surjectivity]
|
||||
\label{conj:simple-md4}
|
||||
For every $n \geq 6$, every minimum-degree-$4$ plane triangulation on $n$
|
||||
vertices is a simple level resolution of some plane triangulation on $n$
|
||||
vertices.
|
||||
\end{conjecture}
|
||||
|
||||
The minimum-degree-$4$ restriction in Conjecture~\ref{conj:simple-md4} is
|
||||
necessary: at $n = 8$, the unique iso-class with three degree-$3$
|
||||
vertices is not reachable by the algorithm; at $n = 10$, two further
|
||||
iso-classes with four degree-$3$ vertices and high-degree hubs fail to
|
||||
appear among algorithm outputs.
|
||||
|
||||
\begin{question}
|
||||
\label{q:terminate-all-n}
|
||||
Does Observation~\ref{obs:terminate} hold for all $n$? Equivalently, does
|
||||
the count of tricky-everywhere odd simple cycles strictly decrease on every
|
||||
Step~2(c) flip, in every plane triangulation?
|
||||
Does Phase~1 terminate for all $(G, S)$? Equivalently, is there an
|
||||
explicit monovariant on $L_k$'s face structure that strictly decreases
|
||||
on every Phase~1 flip?
|
||||
\end{question}
|
||||
|
||||
\section{Discussion and open questions}
|
||||
@@ -493,11 +572,22 @@ The computational results suggest the following:
|
||||
\end{enumerate}
|
||||
|
||||
The algorithm of Section~\ref{sec:flip-algorithm} is the candidate
|
||||
constructive answer: cross-level flips dispose of every odd cycle of $L_k$
|
||||
that admits one, and the facial-depth-guided intra-level flip loop attacks
|
||||
the residual tricky-everywhere cycles. Observation~\ref{obs:terminate}
|
||||
records that the loop terminates on all tested $(G, S, k)$ at $n \leq 11$;
|
||||
Question~\ref{q:terminate-all-n} asks whether termination holds for all $n$.
|
||||
constructive answer. Phase~1 iteratively flips the shared edge between
|
||||
the deepest interior odd face and its lowest-depth neighbor, pushing the
|
||||
residual odd-face structure toward the seed set $\mathcal{B}$ at
|
||||
depth~$0$, with optional even-face restructuring moves along the way;
|
||||
Phase~2 disposes of the remaining outer-incident odd cycles by flipping
|
||||
an outer-face edge each (and optionally an even outer-incident face),
|
||||
accepting a multigraph if the apex edge already exists.
|
||||
Observation~\ref{obs:empirical-lk-bipartite} records that the algorithm
|
||||
terminates and succeeds at the level-bipartiteness layer on all $29640$
|
||||
tested $(G, S, k)$ triples at $n \in \{9, 10, 11\}$.
|
||||
Observation~\ref{obs:md4-simple-resolution} records that every
|
||||
minimum-degree-$4$ iso-class on $n \leq 11$ vertices is reached as a
|
||||
simple level resolution; Conjecture~\ref{conj:simple-md4} extends this
|
||||
to all $n$.
|
||||
Question~\ref{q:terminate-all-n} asks whether Phase~1 terminates in
|
||||
general.
|
||||
|
||||
\section{Implementation}
|
||||
\label{sec:impl}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
Reference in New Issue
Block a user