Small-n bridge-derivability probe: classification + invariant search
Findings at n=9 (50 triangulations, orbits fully exhaustible): - 36 bridge-derived, 14 NOT bridge-derived. So bridge-derived is a PROPER subclass of derived (49 derived at n=9). All 14 non-bridge graphs are intertwining trees -- as are all 50, necessarily: intertwining tree <=> dual Hamiltonian, and the smallest non-Hamiltonian 3-connected cubic planar graph has 38 vertices, i.e. dual on 2n-4=38 => n=21. Hence every triangulation with n<=20 is an intertwining tree, and the disjunction "bridge-derived OR intertwining" is trivially true below n=21. The 4 Holton-McKay duals are the first non-intertwining triangulations. - Static parity-subgraph invariants (Betti numbers, component counts, cross-edge count, existence of an all-forest partition) do NOT separate bridge-derived from non-bridge-derived -- both classes realize beta=0 partitions and identical ranges. Bridge-derivability is dynamical, not a simple static invariant; no easy obstruction. - Side lemma: every valid parity partition of an n-vertex triangulation has exactly 2n-4 cross edges (intra-edges = n-2). Holds for all n=9 graphs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
"""Survey for min-degree-5 maximal planar graphs G such that no Kempe-locked
|
||||
proper 4-coloring of any flip-neighbor of G admits a graph isomorphic to G in
|
||||
its colored edge flip class.
|
||||
|
||||
For each min-degree-5 maximal planar graph G of order n:
|
||||
for each admissible edge uv of G with flip H = G^flip(uv):
|
||||
for each proper 4-coloring phi of H satisfying:
|
||||
(a) phi(u) == phi(v), and
|
||||
(b) for every color b != phi(u), u and v lie in the same connected
|
||||
component of the subgraph of H induced by vertices of color
|
||||
phi(u) or b ({phi(u), b}-Kempe chain),
|
||||
BFS the colored edge flip class C(H, phi) and test each reached graph
|
||||
for isomorphism to G.
|
||||
|
||||
If for some (H, phi) a graph isomorphic to G is reached, G is "found" and we
|
||||
move on. Otherwise we save G and stop.
|
||||
"""
|
||||
from typing import Iterator, Any, cast
|
||||
from sage.all import Graph, graphs # type: ignore[attr-defined] # pylint: disable=no-name-in-module
|
||||
from sage.graphs.graph_coloring import all_graph_colorings # type: ignore[attr-defined] # pylint: disable=no-name-in-module
|
||||
from lib.colored_graphs import canonize_and_save_graph
|
||||
|
||||
|
||||
BFS_LIMIT = 50000
|
||||
|
||||
|
||||
def face_thirds(g: Graph) -> dict[frozenset, tuple]:
|
||||
"""Map each edge {u, v} of triangulation g to (w, x), the two third vertices
|
||||
of the triangular faces incident to uv."""
|
||||
g_emb = g.copy()
|
||||
if not g_emb.is_planar(set_embedding=True):
|
||||
raise ValueError("graph is not planar")
|
||||
incidence: dict[frozenset, list] = {}
|
||||
for face in g_emb.faces():
|
||||
if len(face) != 3:
|
||||
raise ValueError("graph is not a triangulation")
|
||||
a, b, c = face[0][0], face[1][0], face[2][0]
|
||||
for u, v, third in [(a, b, c), (b, c, a), (c, a, b)]:
|
||||
incidence.setdefault(frozenset((u, v)), []).append(third)
|
||||
return {k: tuple(v) for k, v in incidence.items() if len(v) == 2}
|
||||
|
||||
|
||||
def edge_set(g: Graph) -> frozenset:
|
||||
"""Hashable canonical representation of g's edges, ignoring labels."""
|
||||
return frozenset(frozenset(e) for e in g.edges(labels=False))
|
||||
|
||||
|
||||
def kempe_connects(h: Graph, phi: dict[Any, int], a: int, b: int, u: Any, v: Any) -> bool:
|
||||
"""True iff u and v lie in the same component of the subgraph of h
|
||||
induced by vertices of color a or b."""
|
||||
if phi[u] not in (a, b) or phi[v] not in (a, b):
|
||||
return False
|
||||
verts = [w for w in h.vertices() if phi[w] in (a, b)]
|
||||
sub = cast(Graph, h.subgraph(verts))
|
||||
for comp in sub.connected_components():
|
||||
if u in comp:
|
||||
return v in comp
|
||||
return False
|
||||
|
||||
|
||||
def coloring_passes_filter(h: Graph, phi: dict[Any, int], u: Any, v: Any) -> bool:
|
||||
"""phi(u) == phi(v), and for every color b != phi(u), there is a
|
||||
{phi(u), b}-Kempe chain in h from u to v."""
|
||||
a = phi[u]
|
||||
if phi[v] != a:
|
||||
return False
|
||||
for b in range(4):
|
||||
if b == a:
|
||||
continue
|
||||
if not kempe_connects(h, phi, a, b, u, v):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def color_preserving_flips(g: Graph, phi: dict[Any, int]) -> Iterator[Graph]:
|
||||
"""Yield every g^flip(u'v') such that the flip is admissible and the new
|
||||
edge's endpoints w', x' satisfy phi(w') != phi(x') (so phi remains proper)."""
|
||||
pairs = face_thirds(g)
|
||||
for u, v in list(g.edges(labels=False)):
|
||||
thirds = pairs.get(frozenset((u, v)))
|
||||
if thirds is None or len(thirds) != 2:
|
||||
continue
|
||||
w, x = thirds
|
||||
if g.has_edge(w, x):
|
||||
continue
|
||||
if phi[w] == phi[x]:
|
||||
continue
|
||||
flipped = g.copy()
|
||||
flipped.delete_edge(u, v)
|
||||
flipped.add_edge(w, x)
|
||||
yield flipped
|
||||
|
||||
|
||||
def canonical_g6(g: Graph) -> str:
|
||||
"""Canonical graph6 string used to test isomorphism."""
|
||||
return cast(Graph, g.canonical_label()).graph6_string()
|
||||
|
||||
|
||||
def class_contains_iso_to(h: Graph, phi: dict[Any, int], target_canonical: str,
|
||||
limit: int = BFS_LIMIT) -> tuple[bool, int, bool]:
|
||||
"""BFS the colored edge flip class of (h, phi). Return (found, visited_count,
|
||||
hit_limit), where found is True iff some reached graph has canonical
|
||||
graph6_string equal to target_canonical."""
|
||||
visited: set[frozenset] = {edge_set(h)}
|
||||
if canonical_g6(h) == target_canonical:
|
||||
return True, len(visited), False
|
||||
frontier: list[Graph] = [h]
|
||||
while frontier:
|
||||
if len(visited) >= limit:
|
||||
return False, len(visited), True
|
||||
cur = frontier.pop()
|
||||
for nxt in color_preserving_flips(cur, phi):
|
||||
es = edge_set(nxt)
|
||||
if es in visited:
|
||||
continue
|
||||
visited.add(es)
|
||||
if canonical_g6(nxt) == target_canonical:
|
||||
return True, len(visited), False
|
||||
frontier.append(nxt)
|
||||
return False, len(visited), False
|
||||
|
||||
|
||||
def survey_graph(g: Graph, verbose: bool = False) -> tuple[bool, bool]:
|
||||
"""Test whether g admits any Kempe-locked flip witness.
|
||||
|
||||
Returns (filter_nonempty, found_in_class). filter_nonempty is True iff at
|
||||
least one (H, phi) satisfies the Kempe-locked criteria. found_in_class
|
||||
is True iff some such (H, phi) has a graph isomorphic to g in its
|
||||
colored edge flip class. (When filter_nonempty is False, found_in_class
|
||||
is also False but the graph should be SKIPPED, not saved.)"""
|
||||
target_canonical = canonical_g6(g)
|
||||
pairs = face_thirds(g)
|
||||
filter_nonempty = False
|
||||
total_filtered_colorings = 0
|
||||
for u, v in list(g.edges(labels=False)):
|
||||
thirds = pairs.get(frozenset((u, v)))
|
||||
if thirds is None or len(thirds) != 2:
|
||||
continue
|
||||
w, x = thirds
|
||||
if g.has_edge(w, x):
|
||||
continue
|
||||
h = g.copy()
|
||||
h.delete_edge(u, v)
|
||||
h.add_edge(w, x)
|
||||
for phi in all_graph_colorings(h, 4, vertex_color_dict=True):
|
||||
phi = cast(dict, phi)
|
||||
if not coloring_passes_filter(h, phi, u, v):
|
||||
continue
|
||||
filter_nonempty = True
|
||||
total_filtered_colorings += 1
|
||||
found, visited, hit_limit = class_contains_iso_to(h, phi, target_canonical)
|
||||
if verbose:
|
||||
msg = f" edge ({u},{v}), phi#{total_filtered_colorings}: visited={visited}{' (LIMIT)' if hit_limit else ''}"
|
||||
print(f"{msg} -> {'found' if found else 'not found'}")
|
||||
if found:
|
||||
return True, True
|
||||
if verbose and not filter_nonempty:
|
||||
print(" no Kempe-locked colorings — skipping")
|
||||
return filter_nonempty, False
|
||||
|
||||
|
||||
def survey(min_order: int, max_order: int) -> Graph | None:
|
||||
"""Iterate min-degree-5 maximal planar graphs in [min_order, max_order].
|
||||
Return the first G failing the Kempe-locked reachability test, or None."""
|
||||
for n in range(min_order, max_order + 1):
|
||||
print(f"=== order {n} ===")
|
||||
gen = graphs.planar_graphs(
|
||||
n, minimum_connectivity=3, maximum_face_size=3, minimum_degree=5
|
||||
)
|
||||
checked = 0
|
||||
skipped_vacuous = 0
|
||||
for g in gen:
|
||||
checked += 1
|
||||
filter_nonempty, found = survey_graph(g, verbose=True)
|
||||
if not filter_nonempty:
|
||||
skipped_vacuous += 1
|
||||
continue
|
||||
if not found:
|
||||
print(f"order {n}, graph #{checked}: Kempe-locked coloring exists "
|
||||
f"but G NOT reachable; saving and stopping")
|
||||
return g
|
||||
if checked % 10 == 0:
|
||||
print(f" order {n}: checked {checked} graphs, skipped (vacuous) {skipped_vacuous}")
|
||||
print(f" order {n} done: {checked} graphs checked, {skipped_vacuous} skipped (vacuous)")
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
min_order = int(sys.argv[1]) if len(sys.argv) > 1 else 12
|
||||
max_order = int(sys.argv[2]) if len(sys.argv) > 2 else 14
|
||||
result = survey(min_order, max_order)
|
||||
if result is None:
|
||||
print("No witness graph found in the surveyed range.")
|
||||
else:
|
||||
_, graph_dir = canonize_and_save_graph(result)
|
||||
print(f"Saved witness to {graph_dir}")
|
||||
@@ -0,0 +1,134 @@
|
||||
"""Example: colored pentagon reduction on a random 20-vertex triangulation."""
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any, cast, TypedDict, Literal
|
||||
from sage.all import graphs, Graph # type: ignore[attr-defined] # pylint: disable=no-name-in-module
|
||||
from lib.planar_embedding import get_embedding_from_pos
|
||||
from lib.colored_graphs import ColoredGraphId, VertexColoring, save_colored_graph
|
||||
from lib.operations import Operation, save_operation_sequence
|
||||
|
||||
DIR = Path(__file__).parent
|
||||
|
||||
def _neighbors_form_cycle(g: Graph, v: Any) -> bool:
|
||||
"""Return True if the neighbors of v induce a cycle in g."""
|
||||
return bool(cast(Graph, g.subgraph(g.neighbors(v))).is_cycle())
|
||||
|
||||
class PluckMeta(TypedDict):
|
||||
"""Meta information about the pluck operation"""
|
||||
v0: Any
|
||||
|
||||
class PluckOperation(Operation):
|
||||
"""Info about an operation in which a vertex v0 and its incident edges is removed from G"""
|
||||
name: Literal['pluck']
|
||||
meta: PluckMeta
|
||||
|
||||
def pluck(g: Graph, coloring: VertexColoring, v0: Any) -> tuple[Graph, VertexColoring]:
|
||||
"""Delete v0 and all its incident edges from g"""
|
||||
g_prime = g.copy()
|
||||
g_prime.delete_vertex(v0)
|
||||
if (pos := g.get_pos()) is not None:
|
||||
g_prime.set_pos({v: p for v, p in pos.items() if v != v0})
|
||||
g_prime.set_embedding(get_embedding_from_pos(g_prime))
|
||||
coloring_prime = coloring.copy()
|
||||
del coloring_prime[v0]
|
||||
return g_prime, coloring_prime
|
||||
|
||||
|
||||
class SquishMeta(TypedDict):
|
||||
"""Meta information about the squish operation"""
|
||||
v0: Any
|
||||
v_merged: list[Any]
|
||||
|
||||
class SquishOperation(Operation):
|
||||
"""
|
||||
Info about an operation in which two same colored neighbors of a vertex v0 are merged
|
||||
into v0
|
||||
"""
|
||||
name: Literal['squish']
|
||||
meta: SquishMeta
|
||||
|
||||
def squish(g: Graph, coloring: VertexColoring, v0: Any) -> tuple[Graph, VertexColoring, Any, Any]:
|
||||
"""
|
||||
Contract two same-colored neighbors of v0 into v0 and return a new valid
|
||||
coloring along with the new graph.
|
||||
NOTE: assumes g is a maximal planar graph
|
||||
"""
|
||||
neighbor_by_color: defaultdict[Any, list[Any]] = defaultdict(list)
|
||||
for v in g.neighbors(v0):
|
||||
neighbor_by_color[coloring[v]].append(v)
|
||||
|
||||
v1, v2 = next(
|
||||
(vs[0], vs[1]) for vs in neighbor_by_color.values() if len(vs) >= 2
|
||||
)
|
||||
|
||||
g_prime = g.copy()
|
||||
g_prime.merge_vertices([v0, v1, v2])
|
||||
if (pos := g.get_pos()) is not None:
|
||||
g_prime.set_pos({v: p for v, p in pos.items() if v not in (v1, v2)})
|
||||
g_prime.set_embedding(get_embedding_from_pos(g_prime))
|
||||
coloring_prime = {v: c for v, c in coloring.items() if v not in (v1, v2)}
|
||||
coloring_prime[v0] = coloring[v1]
|
||||
return g_prime, coloring_prime, v1, v2
|
||||
|
||||
|
||||
ReduceOperation = SquishOperation | PluckOperation
|
||||
|
||||
def reduce(
|
||||
g: Graph,
|
||||
coloring: VertexColoring,
|
||||
source_canon_id: ColoredGraphId,
|
||||
op_sequence: list[ReduceOperation] | None = None,
|
||||
) -> list[ReduceOperation]:
|
||||
"""Repeatedly apply pluck/squish reductions until no candidates remain."""
|
||||
if op_sequence is None:
|
||||
op_sequence = []
|
||||
|
||||
print(f"Coloring: {coloring}")
|
||||
|
||||
degree_4_candidates: list[Any] = []
|
||||
degree_5_candidates: list[Any] = []
|
||||
|
||||
for v in g.vertices():
|
||||
if g.degree(v) == 3 and _neighbors_form_cycle(g, v):
|
||||
result_graph, result_coloring = pluck(g, coloring, v)
|
||||
print(f"\nG' (after pluck v0={v}): {result_graph.order()} vertices, {result_graph.size()} edges")
|
||||
_, _, result_canon_id = save_colored_graph(result_graph, result_coloring)
|
||||
op_sequence.append(PluckOperation(name='pluck', meta=PluckMeta(v0=v), source_graph=g, source_canon_id=source_canon_id, result_graph=result_graph, result_coloring=result_coloring, result_canon_id=result_canon_id))
|
||||
return reduce(result_graph, result_coloring, result_canon_id, op_sequence)
|
||||
if g.degree(v) == 4 and _neighbors_form_cycle(g, v):
|
||||
degree_4_candidates.append(v)
|
||||
elif g.degree(v) == 5 and _neighbors_form_cycle(g, v):
|
||||
degree_5_candidates.append(v)
|
||||
|
||||
if degree_4_candidates:
|
||||
v0 = degree_4_candidates[0]
|
||||
result_graph, result_coloring, v1, v2 = squish(g, coloring, v0)
|
||||
print(f"Shared-color neighbors: v1={v1}, v2={v2} (color {coloring[v1]})")
|
||||
print(f"\nG' (after squish v0={v0}): {result_graph.order()} vertices, {result_graph.size()} edges")
|
||||
_, _, result_canon_id = save_colored_graph(result_graph, result_coloring)
|
||||
op_sequence.append(SquishOperation(name='squish', meta=SquishMeta(v0=v0, v_merged=[v1, v2]), source_graph=g, source_canon_id=source_canon_id, result_graph=result_graph, result_coloring=result_coloring, result_canon_id=result_canon_id))
|
||||
return reduce(result_graph, result_coloring, result_canon_id, op_sequence)
|
||||
|
||||
if degree_5_candidates:
|
||||
v0 = degree_5_candidates[0]
|
||||
result_graph, result_coloring, v1, v2 = squish(g, coloring, v0)
|
||||
print(f"Shared-color neighbors: v1={v1}, v2={v2} (color {coloring[v1]})")
|
||||
print(f"\nG' (after squish v0={v0}): {result_graph.order()} vertices, {result_graph.size()} edges")
|
||||
_, _, result_canon_id = save_colored_graph(result_graph, result_coloring)
|
||||
op_sequence.append(SquishOperation(name='squish', meta=SquishMeta(v0=v0, v_merged=[v1, v2]), source_graph=g, source_canon_id=source_canon_id, result_graph=result_graph, result_coloring=result_coloring, result_canon_id=result_canon_id))
|
||||
return reduce(result_graph, result_coloring, result_canon_id, op_sequence)
|
||||
|
||||
print("DONE")
|
||||
return op_sequence
|
||||
|
||||
|
||||
G = next(graphs.planar_graphs(20, minimum_degree=5))
|
||||
print(f"G: {G.order()} vertices, {G.size()} edges")
|
||||
print(f"Degree sequence: {sorted(G.degree_sequence(), reverse=True)}")
|
||||
starting_coloring_classes = G.coloring()
|
||||
starting_coloring = {v: i for i, cls in enumerate(starting_coloring_classes) for v in cls}
|
||||
_, _, initial_canon_id = save_colored_graph(G, starting_coloring)
|
||||
G.is_planar(set_embedding=True, set_pos=True)
|
||||
|
||||
op_sequence = reduce(G, starting_coloring, initial_canon_id)
|
||||
save_operation_sequence(op_sequence, G, starting_coloring, DIR)
|
||||
@@ -0,0 +1,71 @@
|
||||
"""Dump candidate invariants for bridge-derived vs non-bridge-derived
|
||||
triangulations at small n, to spot a separating (necessary) condition."""
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, '/Users/didericis/Code/math-research/papers/'
|
||||
'level_resolutions_of_maximal_planar_graphs/experiments')
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import networkx as nx
|
||||
from triangulation_gen import enumerate_all_triangulations
|
||||
from exhaustive_bridge import valid_parity_partitions
|
||||
from small_n_probe import is_bridge_derived
|
||||
|
||||
|
||||
def per_partition_stats(G):
|
||||
"""For each valid partition return dict of invariants."""
|
||||
rows = []
|
||||
for labels in valid_parity_partitions(G):
|
||||
ev = [v for v in G.nodes() if labels[v] == 0]
|
||||
od = [v for v in G.nodes() if labels[v] == 1]
|
||||
SE, SO = G.subgraph(ev), G.subgraph(od)
|
||||
ce = nx.number_connected_components(SE)
|
||||
co = nx.number_connected_components(SO)
|
||||
be = SE.number_of_edges() - len(ev) + ce
|
||||
bo = SO.number_of_edges() - len(od) + co
|
||||
rows.append(dict(be=be, bo=bo, ee=SE.number_of_edges(),
|
||||
eo=SO.number_of_edges(), ce=ce, co=co,
|
||||
ne=len(ev), no=len(od)))
|
||||
return rows
|
||||
|
||||
|
||||
def summarize(G):
|
||||
rows = per_partition_stats(G)
|
||||
tb = [r['be'] + r['bo'] for r in rows]
|
||||
cross = [G.number_of_edges() - r['ee'] - r['eo'] for r in rows]
|
||||
# does some partition make BOTH parity subgraphs forests (be=bo=0)?
|
||||
forests = any(r['be'] == 0 and r['bo'] == 0 for r in rows)
|
||||
return dict(min_tb=min(tb), max_tb=max(tb), nparts=len(rows),
|
||||
min_cross=min(cross), max_cross=max(cross),
|
||||
both_forest=forests)
|
||||
|
||||
|
||||
def main(n):
|
||||
tris = enumerate_all_triangulations(n)
|
||||
print(f'n={n}: {len(tris)} triangulations', flush=True)
|
||||
bd_stats, nb_stats = [], []
|
||||
for G in tris:
|
||||
s = summarize(G)
|
||||
if is_bridge_derived(G):
|
||||
bd_stats.append(s)
|
||||
else:
|
||||
nb_stats.append(s)
|
||||
|
||||
def col(stats, key):
|
||||
return sorted(set(s[key] for s in stats))
|
||||
|
||||
for key in ['min_tb', 'max_tb', 'min_cross', 'max_cross']:
|
||||
print(f' {key:10s} bridge={col(bd_stats,key)} '
|
||||
f'NONbridge={col(nb_stats,key)}', flush=True)
|
||||
print(f' both_forest exists? bridge={sorted(set(s["both_forest"] for s in bd_stats))}'
|
||||
f' NONbridge={sorted(set(s["both_forest"] for s in nb_stats))}', flush=True)
|
||||
# separator check: any min_tb value unique to one class?
|
||||
bd_min = set(s['min_tb'] for s in bd_stats)
|
||||
nb_min = set(s['min_tb'] for s in nb_stats)
|
||||
print(f' min_tb only-in-bridge: {bd_min - nb_min}; '
|
||||
f'only-in-NONbridge: {nb_min - bd_min}', flush=True)
|
||||
print(f' both_forest: bridge {sum(s["both_forest"] for s in bd_stats)}/{len(bd_stats)}, '
|
||||
f'NONbridge {sum(s["both_forest"] for s in nb_stats)}/{len(nb_stats)}', flush=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(int(sys.argv[1]) if len(sys.argv) > 1 else 9)
|
||||
@@ -0,0 +1,76 @@
|
||||
"""At small n (orbits fully exhaustible), classify every triangulation as
|
||||
bridge-derived / derived / intertwining-tree, and tabulate candidate
|
||||
invariants on the parity subgraphs to look for one that separates
|
||||
bridge-derived from the rest.
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, '/Users/didericis/Code/math-research/papers/'
|
||||
'level_resolutions_of_maximal_planar_graphs/experiments')
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import networkx as nx
|
||||
from triangulation_gen import enumerate_all_triangulations
|
||||
from test_conjecture import canonical_sig, is_even_level_graph
|
||||
from exhaustive_bridge import valid_parity_partitions
|
||||
from fast_bridge import EdgeCode
|
||||
from fast_decide import expand_and_check
|
||||
from test_disjunction import is_intertwining_tree
|
||||
|
||||
|
||||
def is_bridge_derived(G, cap=2_000_000):
|
||||
"""Exhaustive: some valid parity partition L has an ELG (parity L) in
|
||||
the backward bridge-orbit of G. Feasible only at small n."""
|
||||
n = G.number_of_nodes()
|
||||
code = EdgeCode(G.nodes())
|
||||
code.state0 = code.state_of(G)
|
||||
for labels in valid_parity_partitions(G):
|
||||
seen = {code.state0}
|
||||
frontier = [code.state0]
|
||||
while frontier and len(seen) < cap:
|
||||
new = []
|
||||
for st in frontier:
|
||||
wit, neigh = expand_and_check(st, code, labels, n)
|
||||
if wit:
|
||||
return True
|
||||
for ns in neigh:
|
||||
if ns not in seen:
|
||||
seen.add(ns)
|
||||
new.append(ns)
|
||||
frontier = new
|
||||
return False
|
||||
|
||||
|
||||
def parity_invariants(G):
|
||||
"""Over all valid parity partitions, collect (betti_even, betti_odd,
|
||||
e_even, e_odd, comps_even, comps_odd) tuples; return the set."""
|
||||
out = set()
|
||||
for labels in valid_parity_partitions(G):
|
||||
ev = [v for v in G.nodes() if labels[v] == 0]
|
||||
od = [v for v in G.nodes() if labels[v] == 1]
|
||||
SE, SO = G.subgraph(ev), G.subgraph(od)
|
||||
be = SE.number_of_edges() - len(ev) + nx.number_connected_components(SE)
|
||||
bo = SO.number_of_edges() - len(od) + nx.number_connected_components(SO)
|
||||
out.add((be, bo, SE.number_of_edges(), SO.number_of_edges(),
|
||||
nx.number_connected_components(SE),
|
||||
nx.number_connected_components(SO)))
|
||||
return out
|
||||
|
||||
|
||||
def main(n):
|
||||
tris = enumerate_all_triangulations(n)
|
||||
print(f'n={n}: {len(tris)} triangulations', flush=True)
|
||||
classes = {'bridge': [], 'not_bridge': []}
|
||||
for idx, G in enumerate(tris):
|
||||
bd = is_bridge_derived(G)
|
||||
it = is_intertwining_tree(G)
|
||||
deg = tuple(sorted((d for _, d in G.degree()), reverse=True))
|
||||
classes['bridge' if bd else 'not_bridge'].append((idx, it, deg))
|
||||
print(f' bridge-derived: {len(classes["bridge"])}', flush=True)
|
||||
print(f' NOT bridge-derived: {len(classes["not_bridge"])}', flush=True)
|
||||
print(' --- NOT bridge-derived (idx, intertwining?, degseq) ---', flush=True)
|
||||
for idx, it, deg in classes['not_bridge']:
|
||||
print(f' idx={idx} intertwining={it} deg={deg}', flush=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(int(sys.argv[1]) if len(sys.argv) > 1 else 9)
|
||||
@@ -0,0 +1,492 @@
|
||||
"""Plane depth sequencing on maximal planar graphs."""
|
||||
import random
|
||||
from typing import Any, TypedDict
|
||||
from sage.all import Graph, graphs # type: ignore[attr-defined] # pylint: disable=no-name-in-module
|
||||
from lib.colored_graphs import canonize_and_save_graph
|
||||
|
||||
|
||||
class DeeplyEmbeddedGraph(TypedDict):
|
||||
graph: Graph
|
||||
outer_cycle: list[Any]
|
||||
plane_depth_labelling: dict[Any, int]
|
||||
deep_embedding: Graph
|
||||
|
||||
|
||||
class QuadrilateralSequence(TypedDict):
|
||||
deep_embedding: Graph
|
||||
triangular_faces: list[frozenset[Any]]
|
||||
depth_labelling: dict[Any, int]
|
||||
outer_cap_vertex: Any
|
||||
quadrilaterals: list[frozenset[frozenset[Any]]]
|
||||
sequence: list[frozenset[frozenset[Any]]]
|
||||
move_codes: list[int]
|
||||
|
||||
|
||||
def get_plane_depth_labelling(g: Graph, outer_cycle: list[Any]) -> dict[Any, int]:
|
||||
"""Return the plane depth of each vertex relative to the given outer cycle."""
|
||||
# equivalent to the commented out naive implementation:
|
||||
# return {v: min(g.distance(v, u) for u in outer_cycle) for v in g.vertices()}
|
||||
source = max(g.vertices()) + 1
|
||||
g_prime = g.copy()
|
||||
g_prime.add_vertex(source)
|
||||
g_prime.add_edges([(source, v) for v in outer_cycle])
|
||||
distances = g_prime.breadth_first_search(source, report_distance=True)
|
||||
return {v: d - 1 for v, d in distances if v != source}
|
||||
|
||||
|
||||
def deep_embedding(g: Graph, outer_cycle: list[Any], plane_depth_labelling: dict[Any, int] | None = None) -> Graph:
|
||||
"""
|
||||
Return the deep embedding of g relative to outer_cycle.
|
||||
|
||||
For every interior triangular face whose three vertices all have the same
|
||||
plane depth, a new vertex is added adjacent to each vertex of that face.
|
||||
"""
|
||||
if plane_depth_labelling is None:
|
||||
plane_depth_labelling = get_plane_depth_labelling(g, outer_cycle)
|
||||
outer_vertices = set(outer_cycle)
|
||||
|
||||
embedding = g.get_embedding()
|
||||
if embedding is None:
|
||||
g.is_planar(set_embedding=True)
|
||||
embedding = g.get_embedding()
|
||||
|
||||
g_prime = g.copy()
|
||||
new_vertex = max(g.vertices()) + 1
|
||||
|
||||
for face in g.faces(embedding):
|
||||
face_vertices = [u for u, v in face]
|
||||
if set(face_vertices) == outer_vertices:
|
||||
continue
|
||||
if plane_depth_labelling[face_vertices[0]] == plane_depth_labelling[face_vertices[1]] == plane_depth_labelling[face_vertices[2]]:
|
||||
g_prime.add_vertex(new_vertex)
|
||||
g_prime.add_edges([(new_vertex, v) for v in face_vertices])
|
||||
new_vertex += 1
|
||||
|
||||
return g_prime
|
||||
|
||||
|
||||
def _triangle_type(face: frozenset[Any], depth_labelling: dict[Any, int]) -> str:
|
||||
"""Return 'up' or 'down' for a triangular face (up: {d, d+1, d+1}; down: {d, d, d+1})."""
|
||||
depths = sorted(depth_labelling[v] for v in face)
|
||||
if depths[0] == depths[1]:
|
||||
return 'down'
|
||||
return 'up'
|
||||
|
||||
|
||||
def _level_edge_of_face(face: frozenset[Any], depth_labelling: dict[Any, int]) -> frozenset[Any]:
|
||||
"""Return the unique level edge of an up/down triangular face."""
|
||||
vs = list(face)
|
||||
for i in range(3):
|
||||
for j in range(i + 1, 3):
|
||||
if depth_labelling[vs[i]] == depth_labelling[vs[j]]:
|
||||
return frozenset([vs[i], vs[j]])
|
||||
raise ValueError(f"Face {set(face)} has no level edge (not up or down)")
|
||||
|
||||
|
||||
def _quad_vertices(quad: frozenset[frozenset[Any]]) -> frozenset[Any]:
|
||||
"""Return the 4 vertices of a quadrilateral."""
|
||||
f1, f2 = list(quad)
|
||||
return f1 | f2
|
||||
|
||||
|
||||
def _quad_perimeter_edges(
|
||||
quad: frozenset[frozenset[Any]],
|
||||
depth_labelling: dict[Any, int],
|
||||
) -> list[frozenset[Any]]:
|
||||
"""Return the 4 perimeter edges (non-level) of a quadrilateral."""
|
||||
f1 = next(iter(quad))
|
||||
level_edge = _level_edge_of_face(f1, depth_labelling)
|
||||
edges: list[frozenset[Any]] = []
|
||||
for f in quad:
|
||||
vs = list(f)
|
||||
for i in range(3):
|
||||
edge = frozenset([vs[i], vs[(i + 1) % 3]])
|
||||
if edge != level_edge:
|
||||
edges.append(edge)
|
||||
return edges
|
||||
|
||||
|
||||
def _quad_type(quad: frozenset[frozenset[Any]], depth_labelling: dict[Any, int]) -> str:
|
||||
"""Return 'shallow_diamond', 'deep_diamond', or 's_quad' for a quadrilateral."""
|
||||
types = tuple(sorted(_triangle_type(f, depth_labelling) for f in quad))
|
||||
if types == ('up', 'up'):
|
||||
return 'shallow_diamond'
|
||||
if types == ('down', 'down'):
|
||||
return 'deep_diamond'
|
||||
return 's_quad'
|
||||
|
||||
|
||||
def extended_deep_embedding(
|
||||
g: Graph,
|
||||
outer_cycle: list[Any],
|
||||
plane_depth_labelling: dict[Any, int] | None = None,
|
||||
) -> tuple[Graph, list[frozenset[Any]], dict[Any, int], Any, dict[Any, list[Any]]]:
|
||||
"""
|
||||
Return the extended deep embedding of g (including the outer face).
|
||||
|
||||
Subdivides every neutral triangular face --- including the outer face,
|
||||
which is always neutral for a maximal planar graph --- by adding a new
|
||||
interior vertex. The vertex added inside the outer face is the
|
||||
outer-cap vertex x*.
|
||||
|
||||
Returns (g_prime, faces, depth_labelling, outer_cap_vertex, embedding) where:
|
||||
- g_prime: the deep embedding graph G'
|
||||
- faces: list of triangular faces of G' (each a frozenset of 3 vertices),
|
||||
derived from Sage's planar embedding of g_prime
|
||||
- depth_labelling: depth of every vertex in G' (extends the original)
|
||||
- outer_cap_vertex: the new vertex placed inside the outer face
|
||||
- embedding: the CCW rotation system of g_prime (vertex -> ordered
|
||||
neighbors) as computed by Sage's planarity routine
|
||||
"""
|
||||
if plane_depth_labelling is None:
|
||||
plane_depth_labelling = get_plane_depth_labelling(g, outer_cycle)
|
||||
outer_vertices = frozenset(outer_cycle)
|
||||
|
||||
embedding_g = g.get_embedding()
|
||||
if embedding_g is None:
|
||||
g.is_planar(set_embedding=True)
|
||||
embedding_g = g.get_embedding()
|
||||
|
||||
g_prime = g.copy()
|
||||
next_vertex = max(g.vertices()) + 1
|
||||
depth_labelling = dict(plane_depth_labelling)
|
||||
outer_cap_vertex: Any = None
|
||||
|
||||
for face in g.faces(embedding_g):
|
||||
face_vertices = [u for u, v in face]
|
||||
a, b, c = face_vertices
|
||||
if depth_labelling[a] == depth_labelling[b] == depth_labelling[c]:
|
||||
x = next_vertex
|
||||
next_vertex += 1
|
||||
g_prime.add_vertex(x)
|
||||
g_prime.add_edges([(x, a), (x, b), (x, c)])
|
||||
depth_labelling[x] = depth_labelling[a] + 1
|
||||
if frozenset(face_vertices) == outer_vertices:
|
||||
outer_cap_vertex = x
|
||||
|
||||
g_prime.is_planar(set_embedding=True)
|
||||
embedding = g_prime.get_embedding()
|
||||
assert embedding is not None, "g_prime must be planar after construction"
|
||||
faces = [frozenset([u for u, v in face]) for face in g_prime.faces(embedding)]
|
||||
|
||||
return g_prime, faces, depth_labelling, outer_cap_vertex, embedding
|
||||
|
||||
|
||||
def quadrilateral_decomposition(
|
||||
faces: list[frozenset[Any]],
|
||||
depth_labelling: dict[Any, int],
|
||||
) -> tuple[list[frozenset[frozenset[Any]]], dict[frozenset[Any], frozenset[frozenset[Any]]]]:
|
||||
"""
|
||||
Pair each face with the face on the other side of its level edge.
|
||||
|
||||
Returns (quads, quad_of_face) where:
|
||||
- quads: list of {F1, F2} pairs (each a quadrilateral of the decomposition)
|
||||
- quad_of_face: dict mapping each face to its containing quad
|
||||
"""
|
||||
faces_by_edge: dict[frozenset[Any], list[frozenset[Any]]] = {}
|
||||
for face in faces:
|
||||
vs = list(face)
|
||||
for i in range(3):
|
||||
edge = frozenset([vs[i], vs[(i + 1) % 3]])
|
||||
faces_by_edge.setdefault(edge, []).append(face)
|
||||
|
||||
quads: list[frozenset[frozenset[Any]]] = []
|
||||
quad_of_face: dict[frozenset[Any], frozenset[frozenset[Any]]] = {}
|
||||
seen: set[frozenset[Any]] = set()
|
||||
for face in faces:
|
||||
if face in seen:
|
||||
continue
|
||||
level_edge = _level_edge_of_face(face, depth_labelling)
|
||||
adjacent = faces_by_edge[level_edge]
|
||||
assert len(adjacent) == 2, f"Level edge {set(level_edge)} has {len(adjacent)} adjacent faces"
|
||||
other = adjacent[0] if adjacent[1] == face else adjacent[1]
|
||||
quad = frozenset([face, other])
|
||||
quads.append(quad)
|
||||
quad_of_face[face] = quad
|
||||
quad_of_face[other] = quad
|
||||
seen.add(face)
|
||||
seen.add(other)
|
||||
|
||||
return quads, quad_of_face
|
||||
|
||||
|
||||
def _boundary_edges(
|
||||
slice_faces: set[frozenset[Any]],
|
||||
faces_by_edge: dict[frozenset[Any], list[frozenset[Any]]],
|
||||
) -> set[frozenset[Any]]:
|
||||
"""Return the set of edges that lie on the boundary of the slice."""
|
||||
boundary: set[frozenset[Any]] = set()
|
||||
for edge, fs in faces_by_edge.items():
|
||||
inside = sum(1 for f in fs if f in slice_faces)
|
||||
if inside == 1:
|
||||
boundary.add(edge)
|
||||
return boundary
|
||||
|
||||
|
||||
def _face_of_wedge(
|
||||
embedding: dict[Any, list[Any]],
|
||||
) -> dict[tuple[Any, Any, Any], frozenset[Any]]:
|
||||
"""For each (v, n_a, n_b) where n_a, n_b are CCW-consecutive in v's rotation,
|
||||
return the triangular face at that wedge."""
|
||||
fow: dict[tuple[Any, Any, Any], frozenset[Any]] = {}
|
||||
for v, rotation in embedding.items():
|
||||
k = len(rotation)
|
||||
for i in range(k):
|
||||
n_a = rotation[i]
|
||||
n_b = rotation[(i + 1) % k]
|
||||
fow[(v, n_a, n_b)] = frozenset({v, n_a, n_b})
|
||||
return fow
|
||||
|
||||
|
||||
def _find_starting_boundary_edge(
|
||||
slice_faces: set[frozenset[Any]],
|
||||
embedding: dict[Any, list[Any]],
|
||||
face_of_wedge: dict[tuple[Any, Any, Any], frozenset[Any]],
|
||||
) -> tuple[Any, Any]:
|
||||
"""Find a directed boundary edge (v, n_b) with the slice on its LEFT."""
|
||||
for v, rotation in embedding.items():
|
||||
k = len(rotation)
|
||||
for i in range(k):
|
||||
n_a = rotation[i]
|
||||
n_b = rotation[(i + 1) % k]
|
||||
n_c = rotation[(i + 2) % k]
|
||||
left_wedge = face_of_wedge[(v, n_a, n_b)]
|
||||
right_wedge = face_of_wedge[(v, n_b, n_c)]
|
||||
if left_wedge in slice_faces and right_wedge not in slice_faces:
|
||||
return (v, n_b)
|
||||
raise RuntimeError("No boundary edge found for slice")
|
||||
|
||||
|
||||
def _boundary_walk(
|
||||
slice_faces: set[frozenset[Any]],
|
||||
embedding: dict[Any, list[Any]],
|
||||
face_of_wedge: dict[tuple[Any, Any, Any], frozenset[Any]],
|
||||
) -> list[tuple[Any, Any]]:
|
||||
"""Trace the slice's boundary CCW with the slice on the LEFT.
|
||||
|
||||
Returns an ordered list of directed edges forming the closed boundary walk.
|
||||
"""
|
||||
start = _find_starting_boundary_edge(slice_faces, embedding, face_of_wedge)
|
||||
walk: list[tuple[Any, Any]] = [start]
|
||||
curr_u, curr_v = start
|
||||
while True:
|
||||
rotation = embedding[curr_v]
|
||||
k = len(rotation)
|
||||
i = rotation.index(curr_u)
|
||||
next_v = None
|
||||
for j in range(1, k + 1):
|
||||
n_jm1 = rotation[(i + j - 1) % k]
|
||||
n_j = rotation[(i + j) % k]
|
||||
if face_of_wedge[(curr_v, n_jm1, n_j)] not in slice_faces:
|
||||
next_v = n_jm1
|
||||
break
|
||||
if next_v is None:
|
||||
raise RuntimeError(f"All wedges at {curr_v} are in slice; no boundary edge")
|
||||
next_edge = (curr_v, next_v)
|
||||
if next_edge == start:
|
||||
break
|
||||
walk.append(next_edge)
|
||||
curr_u, curr_v = next_edge
|
||||
return walk
|
||||
|
||||
|
||||
def _canonicalize_walk(
|
||||
walk: list[tuple[Any, Any]],
|
||||
depth_labelling: dict[Any, int],
|
||||
) -> list[tuple[Any, Any]]:
|
||||
"""Rotate walk to start at the (smallest depth, smallest vertex id) position.
|
||||
|
||||
This fixes the canonical 'top' of the slice for the top-to-bottom scan.
|
||||
"""
|
||||
starts = [(depth_labelling[u], u) for u, _ in walk]
|
||||
canon_idx = min(range(len(walk)), key=lambda i: starts[i])
|
||||
return walk[canon_idx:] + walk[:canon_idx]
|
||||
|
||||
|
||||
def _attachment_position(
|
||||
quad: frozenset[frozenset[Any]],
|
||||
walk: list[tuple[Any, Any]],
|
||||
depth_labelling: dict[Any, int],
|
||||
) -> int:
|
||||
"""Return the latest index in walk at which a perimeter edge of quad appears.
|
||||
|
||||
Realizes 'bottommost attachment on the right boundary scanned top-to-bottom'.
|
||||
Returns -1 if the quad has no perimeter edge on the boundary walk.
|
||||
"""
|
||||
perimeter = {frozenset(e) for e in _quad_perimeter_edges(quad, depth_labelling)}
|
||||
latest = -1
|
||||
for i, (u, v) in enumerate(walk):
|
||||
if frozenset([u, v]) in perimeter:
|
||||
latest = i
|
||||
return latest
|
||||
|
||||
|
||||
def _boundary_deep_diamonds(
|
||||
quads: list[frozenset[frozenset[Any]]],
|
||||
outer_cycle: list[Any],
|
||||
outer_cap_vertex: Any,
|
||||
) -> list[frozenset[frozenset[Any]]]:
|
||||
"""Return the three boundary deep diamonds (each spans an edge of C)."""
|
||||
outer_set = set(outer_cycle)
|
||||
diamonds: list[frozenset[frozenset[Any]]] = []
|
||||
for q in quads:
|
||||
vs = _quad_vertices(q)
|
||||
if outer_cap_vertex in vs and len(vs & outer_set) == 2:
|
||||
diamonds.append(q)
|
||||
assert len(diamonds) == 3, f"Expected 3 boundary deep diamonds, got {len(diamonds)}"
|
||||
return diamonds
|
||||
|
||||
|
||||
def _run_sequence(
|
||||
initial_quad: frozenset[frozenset[Any]],
|
||||
quads: list[frozenset[frozenset[Any]]],
|
||||
depth_labelling: dict[Any, int],
|
||||
faces_by_edge: dict[frozenset[Any], list[frozenset[Any]]],
|
||||
embedding: dict[Any, list[Any]],
|
||||
face_of_wedge: dict[tuple[Any, Any, Any], frozenset[Any]],
|
||||
) -> tuple[list[frozenset[frozenset[Any]]], list[int]]:
|
||||
"""Run the sequencing loop from initial_quad and return (sequence, move_codes)."""
|
||||
sequence: list[frozenset[frozenset[Any]]] = [initial_quad]
|
||||
move_codes: list[int] = []
|
||||
slice_quads: set[frozenset[frozenset[Any]]] = {initial_quad}
|
||||
slice_faces: set[frozenset[Any]] = set(initial_quad)
|
||||
|
||||
while len(slice_quads) < len(quads):
|
||||
slice_v: set[Any] = set().union(*slice_faces)
|
||||
boundary = _boundary_edges(slice_faces, faces_by_edge)
|
||||
walk = _canonicalize_walk(
|
||||
_boundary_walk(slice_faces, embedding, face_of_wedge),
|
||||
depth_labelling,
|
||||
)
|
||||
|
||||
anchor_drop: list[frozenset[frozenset[Any]]] = []
|
||||
level_add: list[frozenset[frozenset[Any]]] = []
|
||||
join: list[frozenset[frozenset[Any]]] = []
|
||||
ring_completion: list[frozenset[frozenset[Any]]] = []
|
||||
|
||||
for q in quads:
|
||||
if q in slice_quads:
|
||||
continue
|
||||
vs = _quad_vertices(q)
|
||||
k = len(vs & slice_v)
|
||||
edges = _quad_perimeter_edges(q, depth_labelling)
|
||||
j = sum(1 for e in edges if e in boundary)
|
||||
qt = _quad_type(q, depth_labelling)
|
||||
|
||||
if qt == 's_quad' and k == 2 and j == 1:
|
||||
anchor_drop.append(q)
|
||||
if k == 3 and j == 2:
|
||||
level_add.append(q)
|
||||
if qt == 'deep_diamond' and k == 2 and j == 1:
|
||||
join.append(q)
|
||||
if k == 4:
|
||||
ring_completion.append(q)
|
||||
|
||||
def pick(cands: list[frozenset[frozenset[Any]]]) -> frozenset[frozenset[Any]]:
|
||||
return max(cands, key=lambda q: _attachment_position(q, walk, depth_labelling))
|
||||
|
||||
if anchor_drop:
|
||||
next_quad = pick(anchor_drop)
|
||||
next_code = 0
|
||||
elif level_add:
|
||||
next_quad = pick(level_add)
|
||||
next_code = 1
|
||||
elif join:
|
||||
next_quad = pick(join)
|
||||
next_code = 2
|
||||
elif ring_completion:
|
||||
next_quad = pick(ring_completion)
|
||||
next_code = 3
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"No applicable move at step {len(sequence)}; slice has {len(slice_quads)}/{len(quads)} quads"
|
||||
)
|
||||
|
||||
sequence.append(next_quad)
|
||||
move_codes.append(next_code)
|
||||
slice_quads.add(next_quad)
|
||||
slice_faces.update(next_quad)
|
||||
|
||||
return sequence, move_codes
|
||||
|
||||
|
||||
def quadrilateral_sequencing(
|
||||
g: Graph,
|
||||
outer_cycle: list[Any],
|
||||
plane_depth_labelling: dict[Any, int] | None = None,
|
||||
) -> QuadrilateralSequence:
|
||||
"""
|
||||
Build the quadrilateral sequence of g relative to outer_cycle.
|
||||
|
||||
Constructs the extended deep embedding G' (including the outer-cap vertex
|
||||
x* in the outer face), decomposes G' into quadrilaterals via level-edge
|
||||
pairing, and produces the deterministic sequence Q_1, ..., Q_N by
|
||||
repeatedly applying the move-selection rule:
|
||||
|
||||
anchor drop (0) > level add (1) > join (2) > ring completion (3).
|
||||
|
||||
Within each move's candidate list, the bottommost attachment on the
|
||||
canonical boundary walk is selected (the largest index in the walk where
|
||||
a perimeter edge of the candidate appears). Among the three boundary deep
|
||||
diamonds, Q_1 is the start that produces the lexicographically smallest
|
||||
move-code string.
|
||||
|
||||
Simplification: anchor drop / join detection uses (k, j) = (vertices in
|
||||
slice, perimeter edges on slice boundary) only; the orientation-specific
|
||||
'left edge = right edge' clause is not separately enforced.
|
||||
"""
|
||||
g_prime, faces, depth_labelling, outer_cap_vertex, embedding = extended_deep_embedding(
|
||||
g, outer_cycle, plane_depth_labelling
|
||||
)
|
||||
quads, _ = quadrilateral_decomposition(faces, depth_labelling)
|
||||
|
||||
faces_by_edge: dict[frozenset[Any], list[frozenset[Any]]] = {}
|
||||
for face in faces:
|
||||
vs = list(face)
|
||||
for i in range(3):
|
||||
edge = frozenset([vs[i], vs[(i + 1) % 3]])
|
||||
faces_by_edge.setdefault(edge, []).append(face)
|
||||
|
||||
face_of_wedge = _face_of_wedge(embedding)
|
||||
candidates = _boundary_deep_diamonds(quads, outer_cycle, outer_cap_vertex)
|
||||
|
||||
best_sequence: list[frozenset[frozenset[Any]]] | None = None
|
||||
best_codes: list[int] | None = None
|
||||
for q1 in candidates:
|
||||
seq, codes = _run_sequence(q1, quads, depth_labelling, faces_by_edge, embedding, face_of_wedge)
|
||||
if best_codes is None or codes < best_codes:
|
||||
best_sequence = seq
|
||||
best_codes = codes
|
||||
assert best_sequence is not None and best_codes is not None
|
||||
|
||||
return QuadrilateralSequence(
|
||||
deep_embedding=g_prime,
|
||||
triangular_faces=faces,
|
||||
depth_labelling=depth_labelling,
|
||||
outer_cap_vertex=outer_cap_vertex,
|
||||
quadrilaterals=quads,
|
||||
sequence=best_sequence,
|
||||
move_codes=best_codes,
|
||||
)
|
||||
|
||||
|
||||
def generate_example(n: int) -> DeeplyEmbeddedGraph:
|
||||
"""Generate a random maximal planar graph of size n and return the triangulation, outer cycle, and deep embedding."""
|
||||
g = graphs.RandomTriangulation(n)
|
||||
g.is_planar(set_embedding=True)
|
||||
embedding = g.get_embedding()
|
||||
faces = g.faces(embedding)
|
||||
outer_cycle = [u for u, v in random.choice(faces)]
|
||||
plane_depth_labelling = get_plane_depth_labelling(g, outer_cycle)
|
||||
return DeeplyEmbeddedGraph(graph=g, outer_cycle=outer_cycle, plane_depth_labelling=plane_depth_labelling, deep_embedding=deep_embedding(g, outer_cycle, plane_depth_labelling))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
example = generate_example(10)
|
||||
result = quadrilateral_sequencing(example['graph'], example['outer_cycle'])
|
||||
canonical, graph_dir = canonize_and_save_graph(example['graph'])
|
||||
(graph_dir / "plane_depth_sequence").mkdir(parents=True, exist_ok=True)
|
||||
print(canonical)
|
||||
print(f"Number of quadrilaterals: {len(result['quadrilaterals'])}")
|
||||
print(f"Sequence length: {len(result['sequence'])}")
|
||||
print(f"Move codes: {result['move_codes']}")
|
||||
@@ -0,0 +1,221 @@
|
||||
"""Generate the labelled quadrilateral sequencing example figure for the paper.
|
||||
|
||||
Renders the deep embedding G' of a small maximal planar graph, fills each
|
||||
quadrilateral with a color encoding its type, and overlays the index it
|
||||
occupies in the canonical sequence Q_1, ..., Q_N. The output is a PDF placed
|
||||
next to paper.tex.
|
||||
"""
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.patches as patches # noqa: E402 pylint: disable=wrong-import-position
|
||||
import matplotlib.pyplot as plt # noqa: E402 pylint: disable=wrong-import-position
|
||||
|
||||
from sage.all import Graph, graphs # type: ignore[attr-defined] # pylint: disable=no-name-in-module,wrong-import-position # noqa: E402
|
||||
from sage.misc.randstate import set_random_seed # type: ignore[import-not-found] # noqa: E402
|
||||
|
||||
from lib.tutte_embedding import tutte_embedding # noqa: E402
|
||||
from plane_depth_sequencing import ( # noqa: E402
|
||||
_level_edge_of_face,
|
||||
_quad_type,
|
||||
_quad_vertices,
|
||||
quadrilateral_sequencing,
|
||||
)
|
||||
|
||||
|
||||
def _planar_pos(
|
||||
g_prime: Graph,
|
||||
outer_face: list[Any],
|
||||
layout: str,
|
||||
) -> dict[Any, tuple[float, float]]:
|
||||
"""Compute positions for g_prime under either 'tutte' or 'sage_planar' layout."""
|
||||
if layout == "tutte":
|
||||
return tutte_embedding(g_prime, outer_face)
|
||||
if layout == "sage_planar":
|
||||
g_prime.is_planar(set_embedding=True, set_pos=True)
|
||||
pos = g_prime.get_pos()
|
||||
assert pos is not None, "Sage failed to set planar positions"
|
||||
return {v: (float(p[0]), float(p[1])) for v, p in pos.items()}
|
||||
raise ValueError(f"Unknown layout: {layout}")
|
||||
|
||||
|
||||
MOVE_NAMES = {0: "AD", 1: "LA", 2: "J", 3: "RC"}
|
||||
TYPE_COLORS = {
|
||||
"shallow_diamond": "#FFE0B2",
|
||||
"deep_diamond": "#B2DFDB",
|
||||
"s_quad": "#F8BBD0",
|
||||
}
|
||||
|
||||
|
||||
def _ordered_quad_vertices(quad: frozenset, depth_labelling: dict[Any, int]) -> list[Any]:
|
||||
"""Return the 4 quad vertices in cyclic order: e1, a, e2, b."""
|
||||
f1, f2 = list(quad)
|
||||
level_edge = _level_edge_of_face(f1, depth_labelling)
|
||||
e1, e2 = list(level_edge)
|
||||
a = next(v for v in f1 if v not in level_edge)
|
||||
b = next(v for v in f2 if v not in level_edge)
|
||||
return [e1, a, e2, b]
|
||||
|
||||
|
||||
def _pick_outer_cap_face(
|
||||
g_prime: Graph,
|
||||
outer_cap_vertex: Any,
|
||||
outer_cycle: list[Any],
|
||||
) -> list[Any]:
|
||||
"""Return the vertex list of an outer-cap face (the face used as outer in the plane drawing)."""
|
||||
g_prime.is_planar(set_embedding=True)
|
||||
embedding = g_prime.get_embedding()
|
||||
outer_set = set(outer_cycle)
|
||||
for face in g_prime.faces(embedding):
|
||||
verts = [u for u, _ in face]
|
||||
if outer_cap_vertex in verts and len(set(verts) & outer_set) == 2:
|
||||
return verts
|
||||
raise RuntimeError("No outer-cap face found in G' embedding")
|
||||
|
||||
|
||||
def _draw_figure(
|
||||
g: Graph,
|
||||
outer_cycle: list[Any],
|
||||
out_path: Path,
|
||||
layout: str = "sage_planar",
|
||||
) -> dict[str, Any]:
|
||||
"""Render and save the labelled sequencing figure. Returns summary stats."""
|
||||
result = quadrilateral_sequencing(g, outer_cycle)
|
||||
g_prime: Graph = result["deep_embedding"]
|
||||
depth_labelling: dict[Any, int] = result["depth_labelling"]
|
||||
sequence = result["sequence"]
|
||||
move_codes = result["move_codes"]
|
||||
outer_cap_vertex = result["outer_cap_vertex"]
|
||||
|
||||
outer_face = _pick_outer_cap_face(g_prime, outer_cap_vertex, outer_cycle)
|
||||
pos = _planar_pos(g_prime, outer_face, layout)
|
||||
|
||||
fig, ax = plt.subplots(figsize=(9, 9))
|
||||
|
||||
for i, quad in enumerate(sequence):
|
||||
ordered = _ordered_quad_vertices(quad, depth_labelling)
|
||||
poly_pts = [pos[v] for v in ordered]
|
||||
qt = _quad_type(quad, depth_labelling)
|
||||
polygon = patches.Polygon(
|
||||
poly_pts,
|
||||
closed=True,
|
||||
facecolor=TYPE_COLORS[qt],
|
||||
edgecolor="none",
|
||||
alpha=0.65,
|
||||
zorder=1,
|
||||
)
|
||||
ax.add_patch(polygon)
|
||||
|
||||
for u, v in g_prime.edges(labels=False):
|
||||
x1, y1 = pos[u]
|
||||
x2, y2 = pos[v]
|
||||
if depth_labelling[u] == depth_labelling[v]:
|
||||
ax.plot([x1, x2], [y1, y2], linestyle=(0, (3, 2)), color="#666666", linewidth=1.0, zorder=2)
|
||||
else:
|
||||
ax.plot([x1, x2], [y1, y2], "-", color="black", linewidth=1.0, zorder=2)
|
||||
|
||||
outer_set = set(outer_cycle)
|
||||
for v, (x, y) in pos.items():
|
||||
if v == outer_cap_vertex:
|
||||
color = "#D32F2F"
|
||||
elif v in outer_set:
|
||||
color = "#1976D2"
|
||||
else:
|
||||
color = "black"
|
||||
ax.scatter([x], [y], s=28, color=color, zorder=5, edgecolors="white", linewidths=0.8)
|
||||
name = "$x^{*}$" if v == outer_cap_vertex else str(v)
|
||||
ax.annotate(
|
||||
name,
|
||||
(x, y),
|
||||
xytext=(7, 6),
|
||||
textcoords="offset points",
|
||||
fontsize=9,
|
||||
zorder=8,
|
||||
color="#222222",
|
||||
bbox={"boxstyle": "round,pad=0.05", "facecolor": "white", "edgecolor": "none", "alpha": 0.85},
|
||||
)
|
||||
|
||||
for i, quad in enumerate(sequence):
|
||||
ordered = _ordered_quad_vertices(quad, depth_labelling)
|
||||
poly_pts = [pos[v] for v in ordered]
|
||||
cx = sum(p[0] for p in poly_pts) / 4
|
||||
cy = sum(p[1] for p in poly_pts) / 4
|
||||
if i == 0:
|
||||
label = "$Q_{1}$"
|
||||
else:
|
||||
label = f"$Q_{{{i + 1}}}^{{\\mathrm{{{MOVE_NAMES[move_codes[i - 1]]}}}}}$"
|
||||
ax.text(
|
||||
cx,
|
||||
cy,
|
||||
label,
|
||||
ha="center",
|
||||
va="center",
|
||||
fontsize=12,
|
||||
zorder=7,
|
||||
bbox={"boxstyle": "round,pad=0.22", "facecolor": "white", "edgecolor": "#444444", "alpha": 0.95},
|
||||
)
|
||||
|
||||
legend_handles = [
|
||||
patches.Patch(color=TYPE_COLORS["shallow_diamond"], label="shallow diamond"),
|
||||
patches.Patch(color=TYPE_COLORS["deep_diamond"], label="deep diamond"),
|
||||
patches.Patch(color=TYPE_COLORS["s_quad"], label="S quad"),
|
||||
]
|
||||
ax.legend(handles=legend_handles, loc="lower right", fontsize=10, framealpha=0.95)
|
||||
|
||||
ax.set_aspect("equal")
|
||||
ax.axis("off")
|
||||
plt.tight_layout()
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
plt.savefig(out_path, format="pdf", bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
|
||||
return {
|
||||
"n_vertices_g": g.order(),
|
||||
"n_vertices_gprime": g_prime.order(),
|
||||
"n_quads": len(sequence),
|
||||
"move_codes": move_codes,
|
||||
"outer_cycle": outer_cycle,
|
||||
"outer_cap_vertex": outer_cap_vertex,
|
||||
"depth_labelling": depth_labelling,
|
||||
}
|
||||
|
||||
|
||||
def _build_example(seed: int, n: int) -> tuple[Graph, list[Any]]:
|
||||
"""Build a random triangulation and pick a deterministic outer cycle."""
|
||||
set_random_seed(seed)
|
||||
g = graphs.RandomTriangulation(n)
|
||||
g.is_planar(set_embedding=True)
|
||||
embedding = g.get_embedding()
|
||||
faces = g.faces(embedding)
|
||||
outer_cycle = [u for u, _ in faces[0]]
|
||||
return g, outer_cycle
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--seed", type=int, default=141)
|
||||
parser.add_argument("--n", type=int, default=9)
|
||||
parser.add_argument(
|
||||
"--out",
|
||||
type=Path,
|
||||
default=Path("papers/plane_depth_sequencing/example_figure.pdf"),
|
||||
)
|
||||
parser.add_argument("--layout", choices=("tutte", "sage_planar"), default="sage_planar")
|
||||
args = parser.parse_args()
|
||||
|
||||
g, outer_cycle = _build_example(args.seed, args.n)
|
||||
summary = _draw_figure(g, outer_cycle, args.out, layout=args.layout)
|
||||
|
||||
print(f"Wrote {args.out}")
|
||||
print(f" |V(G)|={summary['n_vertices_g']}, |V(G')|={summary['n_vertices_gprime']}, "
|
||||
f"N={summary['n_quads']}")
|
||||
print(f" outer cycle: {summary['outer_cycle']}, x* = {summary['outer_cap_vertex']}")
|
||||
code_str = "".join(str(c) for c in summary["move_codes"])
|
||||
print(f" move-code string: {code_str}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,155 @@
|
||||
"""Empirical check: does pure greedy online 4-coloring succeed under the canonical quadrilateral sequencing?
|
||||
|
||||
The user's conjecture-of-the-day: for the canonical sequence, every non-ring-completion
|
||||
move (anchor drop, level add, join) admits a local 4-coloring choice, and ring completions
|
||||
introduce no new vertices, so the only question is whether the existing coloring agrees
|
||||
on the ring-completion quad's edges.
|
||||
|
||||
Under proper greedy online coloring this latter check is automatic: every G'-edge
|
||||
constraint is enforced as the second endpoint is colored. So the empirical question
|
||||
becomes: does pure greedy (smallest color avoiding already-colored G'-neighbors) ever
|
||||
get stuck at a non-ring-completion move with all four colors forbidden?
|
||||
|
||||
We run greedy in a canonical vertex order (depth, then vertex id) and classify every
|
||||
failure as one of:
|
||||
- 'no_color_available': a new vertex saw all 4 colors among colored G'-neighbors
|
||||
- 'ring_completion_monochromatic': a ring completion's quad has a monochromatic edge
|
||||
- 'success': global proper 4-coloring of G'.
|
||||
"""
|
||||
from typing import Any
|
||||
from sage.all import Graph, graphs # type: ignore[attr-defined] # pylint: disable=no-name-in-module
|
||||
from lib.colored_graphs import canonize_and_save_graph
|
||||
from plane_depth_sequencing import (
|
||||
quadrilateral_sequencing,
|
||||
_quad_vertices,
|
||||
)
|
||||
|
||||
|
||||
def greedy_4_color_under_sequence(
|
||||
g_prime: Graph,
|
||||
sequence: list[frozenset[frozenset[Any]]],
|
||||
depth_labelling: dict[Any, int],
|
||||
) -> tuple[dict[Any, int], str, int | None]:
|
||||
"""Run greedy online 4-coloring under the canonical sequence.
|
||||
|
||||
Returns (partial_coloring, status, failing_step).
|
||||
status is one of:
|
||||
- 'success'
|
||||
- 'no_color_available' (failing_step is the sequence index)
|
||||
- 'ring_completion_monochromatic' (failing_step is the sequence index)
|
||||
"""
|
||||
coloring: dict[Any, int] = {}
|
||||
slice_vertices: set[Any] = set()
|
||||
adj: dict[Any, set[Any]] = {v: set(g_prime.neighbors(v)) for v in g_prime.vertices()}
|
||||
|
||||
for step, quad in enumerate(sequence):
|
||||
quad_v = _quad_vertices(quad)
|
||||
new_vertices = quad_v - slice_vertices
|
||||
|
||||
if not new_vertices:
|
||||
# Ring completion: verify the existing coloring is proper on the quad's edges.
|
||||
quad_v_list = list(quad_v)
|
||||
for i in range(len(quad_v_list)):
|
||||
for j in range(i + 1, len(quad_v_list)):
|
||||
u, v = quad_v_list[i], quad_v_list[j]
|
||||
if v in adj[u] and coloring[u] == coloring[v]:
|
||||
return coloring, 'ring_completion_monochromatic', step
|
||||
continue
|
||||
|
||||
# Color new vertices in a canonical order (depth, then vertex id).
|
||||
for v in sorted(new_vertices, key=lambda x: (depth_labelling[x], x)):
|
||||
used = {coloring[u] for u in adj[v] if u in coloring}
|
||||
avail = [c for c in (0, 1, 2, 3) if c not in used]
|
||||
if not avail:
|
||||
return coloring, 'no_color_available', step
|
||||
coloring[v] = min(avail)
|
||||
slice_vertices.add(v)
|
||||
|
||||
return coloring, 'success', None
|
||||
|
||||
|
||||
def _outer_cycle_from_embedding(g: Graph) -> list[Any]:
|
||||
"""Pick a deterministic outer cycle: the first face Sage returns from the embedding."""
|
||||
g.is_planar(set_embedding=True)
|
||||
embedding = g.get_embedding()
|
||||
faces = g.faces(embedding)
|
||||
return [u for u, v in faces[0]]
|
||||
|
||||
|
||||
def check_graph(g: Graph) -> dict[str, Any]:
|
||||
"""Run sequencing + greedy coloring on a single graph. Returns a result dict."""
|
||||
outer_cycle = _outer_cycle_from_embedding(g)
|
||||
try:
|
||||
seq_result = quadrilateral_sequencing(g, outer_cycle)
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
return {
|
||||
'graph6': g.graph6_string(),
|
||||
'status': 'sequencing_error',
|
||||
'error': str(exc),
|
||||
}
|
||||
coloring, status, failing_step = greedy_4_color_under_sequence(
|
||||
seq_result['deep_embedding'],
|
||||
seq_result['sequence'],
|
||||
seq_result['depth_labelling'],
|
||||
)
|
||||
return {
|
||||
'graph6': g.graph6_string(),
|
||||
'status': status,
|
||||
'failing_step': failing_step,
|
||||
'coloring_size': len(coloring),
|
||||
'num_quads': len(seq_result['quadrilaterals']),
|
||||
'move_codes': seq_result['move_codes'],
|
||||
}
|
||||
|
||||
|
||||
def run_enumeration(min_order: int, max_order: int) -> dict[int, dict[str, Any]]:
|
||||
"""Iterate over all 3-connected triangulations of order in [min_order, max_order]."""
|
||||
summary: dict[int, dict[str, Any]] = {}
|
||||
for n in range(min_order, max_order + 1):
|
||||
total = 0
|
||||
by_status: dict[str, int] = {}
|
||||
failures: list[dict[str, Any]] = []
|
||||
for g in graphs.planar_graphs(n, minimum_connectivity=3, maximum_face_size=3):
|
||||
total += 1
|
||||
result = check_graph(g)
|
||||
status = result['status']
|
||||
by_status[status] = by_status.get(status, 0) + 1
|
||||
if status != 'success':
|
||||
failures.append(result)
|
||||
if total % 200 == 0:
|
||||
print(f" n={n}: checked {total}, statuses so far: {by_status}")
|
||||
summary[n] = {'total': total, 'by_status': by_status, 'failures': failures}
|
||||
print(f"n={n}: total={total}, by_status={by_status}")
|
||||
for f in failures[:5]:
|
||||
print(f" failure {f['graph6']}: {f['status']} at step {f['failing_step']}")
|
||||
if len(failures) > 5:
|
||||
print(f" ... ({len(failures) - 5} more failures)")
|
||||
return summary
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
min_order = int(sys.argv[1]) if len(sys.argv) > 1 else 4
|
||||
max_order = int(sys.argv[2]) if len(sys.argv) > 2 else 9
|
||||
|
||||
summary = run_enumeration(min_order, max_order)
|
||||
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("Final summary:")
|
||||
for n, s in summary.items():
|
||||
print(f" n={n}: total={s['total']}, by_status={s['by_status']}")
|
||||
|
||||
# Save up to first 10 failure graphs per order for inspection.
|
||||
saved = 0
|
||||
for n, s in summary.items():
|
||||
for f in s['failures'][:10]:
|
||||
try:
|
||||
g = Graph(f['graph6'])
|
||||
canonical, graph_dir = canonize_and_save_graph(g)
|
||||
print(f" saved n={n} {f['graph6']} ({f['status']}) -> {graph_dir}")
|
||||
saved += 1
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
print(f" failed to save {f['graph6']}: {exc}")
|
||||
print(f"Saved {saved} failure graphs total.")
|
||||
@@ -0,0 +1,129 @@
|
||||
"""Plane diamond coloring on maximal planar graphs."""
|
||||
from typing import Any
|
||||
from sage.all import Graph, graphs # type: ignore[attr-defined] # pylint: disable=no-name-in-module
|
||||
from lib.colored_graphs import canonize_and_save_graph
|
||||
|
||||
|
||||
def get_plane_diamond_scaffold(g: Graph, v: Any) -> Graph:
|
||||
"""
|
||||
Return the diamond scaffold of g relative to v.
|
||||
|
||||
The diamond scaffold is the spanning subgraph of g obtained by removing
|
||||
every edge whose endpoints lie in the same level of the distance
|
||||
partition of g from v.
|
||||
"""
|
||||
distances = dict(g.breadth_first_search(v, report_distance=True))
|
||||
scaffold = g.copy()
|
||||
scaffold.delete_edges([(x, y) for x, y in g.edges(labels=False) if distances[x] == distances[y]])
|
||||
return scaffold
|
||||
|
||||
|
||||
def has_plane_diamond_coloring_at_root(g: Graph, u: Any) -> bool:
|
||||
"""
|
||||
Return True iff g admits a proper 4-coloring with two color classes
|
||||
parity-separated by the BFS distance partition from u.
|
||||
|
||||
Equivalent to 4-colorability of the auxiliary graph H_u obtained by
|
||||
adjoining vertices v_a, v_b to g, with v_a adjacent to every odd-parity
|
||||
layer vertex, v_b adjacent to every even-parity layer vertex, and a
|
||||
v_a v_b edge.
|
||||
"""
|
||||
distances = dict(g.breadth_first_search(u, report_distance=True))
|
||||
odd_vertices = [v for v in g.vertices() if distances[v] % 2 == 1]
|
||||
even_vertices = [v for v in g.vertices() if distances[v] % 2 == 0]
|
||||
h = g.copy()
|
||||
v_a = max(g.vertices()) + 1
|
||||
v_b = v_a + 1
|
||||
h.add_vertex(v_a)
|
||||
h.add_vertex(v_b)
|
||||
h.add_edges([(v_a, w) for w in odd_vertices])
|
||||
h.add_edges([(v_b, w) for w in even_vertices])
|
||||
h.add_edge(v_a, v_b)
|
||||
return h.chromatic_number() <= 4
|
||||
|
||||
|
||||
def has_plane_diamond_coloring(g: Graph) -> bool:
|
||||
"""Return True iff some root vertex of g witnesses a plane diamond coloring."""
|
||||
return any(has_plane_diamond_coloring_at_root(g, u) for u in g.vertices())
|
||||
|
||||
|
||||
def search_counterexample(n: int, num_trials: int) -> Graph | None:
|
||||
"""
|
||||
Sample random maximal planar graphs of order n and return the first one
|
||||
with no plane diamond coloring, or None if none is found.
|
||||
"""
|
||||
for trial in range(num_trials):
|
||||
g = graphs.RandomTriangulation(n)
|
||||
if not has_plane_diamond_coloring(g):
|
||||
print(f"Counterexample found on trial {trial + 1}")
|
||||
return g
|
||||
if (trial + 1) % 10 == 0:
|
||||
print(f"Checked {trial + 1}/{num_trials} graphs of order {n}, no counterexample yet")
|
||||
return None
|
||||
|
||||
|
||||
def search_counterexample_comprehensive(max_order: int, min_order: int = 4) -> list[Graph]:
|
||||
"""
|
||||
Iterate through every maximal planar graph of order in [min_order, max_order]
|
||||
and return all those without a plane diamond coloring.
|
||||
"""
|
||||
counterexamples: list[Graph] = []
|
||||
for n in range(min_order, max_order + 1):
|
||||
checked = 0
|
||||
for g in graphs.planar_graphs(n, minimum_connectivity=3, maximum_face_size=3):
|
||||
checked += 1
|
||||
if not has_plane_diamond_coloring(g):
|
||||
print(f"Counterexample at order {n} (graph #{checked}): {g.graph6_string()}")
|
||||
counterexamples.append(g)
|
||||
if checked % 100 == 0:
|
||||
print(f" order {n}: checked {checked} graphs, {len(counterexamples)} counterexamples so far")
|
||||
print(f"order {n} done: {checked} triangulations checked")
|
||||
return counterexamples
|
||||
|
||||
|
||||
def search_min_degree_counterexample_comprehensive(max_order: int, minimum_degree: int, min_order: int = 4) -> list[Graph]:
|
||||
"""
|
||||
Iterate through every maximal planar graph of order in [min_order, max_order]
|
||||
with the given minimum degree, and return all those without a plane diamond
|
||||
coloring.
|
||||
"""
|
||||
counterexamples: list[Graph] = []
|
||||
for n in range(min_order, max_order + 1):
|
||||
checked = 0
|
||||
for g in graphs.planar_graphs(n, minimum_connectivity=3, maximum_face_size=3, minimum_degree=minimum_degree):
|
||||
checked += 1
|
||||
if not has_plane_diamond_coloring(g):
|
||||
print(f"Counterexample at order {n}, min_degree {minimum_degree} (graph #{checked}): {g.graph6_string()}")
|
||||
counterexamples.append(g)
|
||||
if checked % 100 == 0:
|
||||
print(f" order {n}: checked {checked} graphs, {len(counterexamples)} counterexamples so far")
|
||||
print(f"order {n} done: {checked} triangulations of min degree {minimum_degree} checked")
|
||||
return counterexamples
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "comprehensive":
|
||||
max_order = int(sys.argv[2]) if len(sys.argv) > 2 else 10
|
||||
min_order = int(sys.argv[3]) if len(sys.argv) > 3 else 4
|
||||
counterexamples = search_counterexample_comprehensive(max_order, min_order)
|
||||
print(f"Found {len(counterexamples)} counterexamples in orders {min_order}..{max_order}")
|
||||
for g in counterexamples:
|
||||
canonize_and_save_graph(g)
|
||||
elif len(sys.argv) > 1 and sys.argv[1] == "min-degree":
|
||||
max_order = int(sys.argv[2]) if len(sys.argv) > 2 else 13
|
||||
minimum_degree = int(sys.argv[3]) if len(sys.argv) > 3 else 5
|
||||
min_order = int(sys.argv[4]) if len(sys.argv) > 4 else 4
|
||||
counterexamples = search_min_degree_counterexample_comprehensive(max_order, minimum_degree, min_order)
|
||||
print(f"Found {len(counterexamples)} counterexamples in orders {min_order}..{max_order} with min degree {minimum_degree}")
|
||||
for g in counterexamples:
|
||||
canonize_and_save_graph(g)
|
||||
else:
|
||||
n = int(sys.argv[1]) if len(sys.argv) > 1 else 12
|
||||
num_trials = int(sys.argv[2]) if len(sys.argv) > 2 else 100
|
||||
counterexample = search_counterexample(n, num_trials)
|
||||
if counterexample is None:
|
||||
print(f"No counterexample found in {num_trials} random triangulations of order {n}")
|
||||
else:
|
||||
canonical, graph_dir = canonize_and_save_graph(counterexample)
|
||||
print(f"Counterexample saved to {graph_dir}")
|
||||
Reference in New Issue
Block a user