diff --git a/papers/level_resolutions_of_maximal_planar_graphs/experiments/debug_compare_phase1.py b/papers/level_resolutions_of_maximal_planar_graphs/experiments/debug_compare_phase1.py new file mode 100644 index 0000000..dfc6806 --- /dev/null +++ b/papers/level_resolutions_of_maximal_planar_graphs/experiments/debug_compare_phase1.py @@ -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) diff --git a/papers/level_resolutions_of_maximal_planar_graphs/experiments/debug_phase1_budget.py b/papers/level_resolutions_of_maximal_planar_graphs/experiments/debug_phase1_budget.py new file mode 100644 index 0000000..86c970f --- /dev/null +++ b/papers/level_resolutions_of_maximal_planar_graphs/experiments/debug_phase1_budget.py @@ -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 diff --git a/papers/level_resolutions_of_maximal_planar_graphs/experiments/debug_phase2_failure.py b/papers/level_resolutions_of_maximal_planar_graphs/experiments/debug_phase2_failure.py new file mode 100644 index 0000000..9d6cdb1 --- /dev/null +++ b/papers/level_resolutions_of_maximal_planar_graphs/experiments/debug_phase2_failure.py @@ -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}") diff --git a/papers/level_resolutions_of_maximal_planar_graphs/experiments/full_resolution_check.py b/papers/level_resolutions_of_maximal_planar_graphs/experiments/full_resolution_check.py new file mode 100644 index 0000000..c67cc45 --- /dev/null +++ b/papers/level_resolutions_of_maximal_planar_graphs/experiments/full_resolution_check.py @@ -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) diff --git a/papers/level_resolutions_of_maximal_planar_graphs/experiments/inductive_lift_check.py b/papers/level_resolutions_of_maximal_planar_graphs/experiments/inductive_lift_check.py new file mode 100644 index 0000000..35b016b --- /dev/null +++ b/papers/level_resolutions_of_maximal_planar_graphs/experiments/inductive_lift_check.py @@ -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() diff --git a/papers/level_resolutions_of_maximal_planar_graphs/experiments/inspect_iso_5_n8.py b/papers/level_resolutions_of_maximal_planar_graphs/experiments/inspect_iso_5_n8.py new file mode 100644 index 0000000..54a9ae4 --- /dev/null +++ b/papers/level_resolutions_of_maximal_planar_graphs/experiments/inspect_iso_5_n8.py @@ -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}") diff --git a/papers/level_resolutions_of_maximal_planar_graphs/experiments/plot_unreached_n10.py b/papers/level_resolutions_of_maximal_planar_graphs/experiments/plot_unreached_n10.py new file mode 100644 index 0000000..24e2f79 --- /dev/null +++ b/papers/level_resolutions_of_maximal_planar_graphs/experiments/plot_unreached_n10.py @@ -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)}") diff --git a/papers/level_resolutions_of_maximal_planar_graphs/experiments/simple_level_resolution_coverage.py b/papers/level_resolutions_of_maximal_planar_graphs/experiments/simple_level_resolution_coverage.py new file mode 100644 index 0000000..e4bf0fa --- /dev/null +++ b/papers/level_resolutions_of_maximal_planar_graphs/experiments/simple_level_resolution_coverage.py @@ -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) diff --git a/papers/level_resolutions_of_maximal_planar_graphs/experiments/viz_phase2_gap.py b/papers/level_resolutions_of_maximal_planar_graphs/experiments/viz_phase2_gap.py new file mode 100644 index 0000000..b060ebd --- /dev/null +++ b/papers/level_resolutions_of_maximal_planar_graphs/experiments/viz_phase2_gap.py @@ -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() diff --git a/papers/level_resolutions_of_maximal_planar_graphs/paper.aux b/papers/level_resolutions_of_maximal_planar_graphs/paper.aux index e483b73..1ed9929 100644 --- a/papers/level_resolutions_of_maximal_planar_graphs/paper.aux +++ b/papers/level_resolutions_of_maximal_planar_graphs/paper.aux @@ -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} diff --git a/papers/level_resolutions_of_maximal_planar_graphs/paper.log b/papers/level_resolutions_of_maximal_planar_graphs/paper.log index 62edb4d..4673c2b 100644 --- a/papers/level_resolutions_of_maximal_planar_graphs/paper.log +++ b/papers/level_resolutions_of_maximal_planar_graphs/paper.log @@ -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 @@ -393,10 +402,10 @@ ve/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy6.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) diff --git a/papers/level_resolutions_of_maximal_planar_graphs/paper.out b/papers/level_resolutions_of_maximal_planar_graphs/paper.out index f4a67ea..ad6572b 100644 --- a/papers/level_resolutions_of_maximal_planar_graphs/paper.out +++ b/papers/level_resolutions_of_maximal_planar_graphs/paper.out @@ -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 diff --git a/papers/level_resolutions_of_maximal_planar_graphs/paper.pdf b/papers/level_resolutions_of_maximal_planar_graphs/paper.pdf index eaf8687..18ba269 100644 Binary files a/papers/level_resolutions_of_maximal_planar_graphs/paper.pdf and b/papers/level_resolutions_of_maximal_planar_graphs/paper.pdf differ diff --git a/papers/level_resolutions_of_maximal_planar_graphs/paper.tex b/papers/level_resolutions_of_maximal_planar_graphs/paper.tex index 2049ab1..436608a 100644 --- a/papers/level_resolutions_of_maximal_planar_graphs/paper.tex +++ b/papers/level_resolutions_of_maximal_planar_graphs/paper.tex @@ -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} diff --git a/papers/level_resolutions_of_maximal_planar_graphs/phase2_gap_visualization.png b/papers/level_resolutions_of_maximal_planar_graphs/phase2_gap_visualization.png new file mode 100644 index 0000000..8e4a366 Binary files /dev/null and b/papers/level_resolutions_of_maximal_planar_graphs/phase2_gap_visualization.png differ diff --git a/papers/level_resolutions_of_maximal_planar_graphs/unreached_iso_n10.png b/papers/level_resolutions_of_maximal_planar_graphs/unreached_iso_n10.png new file mode 100644 index 0000000..fe07274 Binary files /dev/null and b/papers/level_resolutions_of_maximal_planar_graphs/unreached_iso_n10.png differ