Add planar counterexample figure

This commit is contained in:
2026-06-01 13:10:45 -04:00
parent 15fc7c3b8f
commit 7e684e41a0
5 changed files with 330 additions and 0 deletions
@@ -0,0 +1,132 @@
"""Draw the 8-vertex counterexample to the universal-source form.
The figure highlights the source vertex 7, the two BFS levels, and the
level cycle (3,4,5,8) that forces all four colours for that source.
"""
from __future__ import annotations
import os
import matplotlib.pyplot as plt
import networkx as nx
from matplotlib.lines import Line2D
OUT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def build_graph() -> nx.Graph:
g = nx.Graph()
g.add_edges_from(
[
(1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7),
(2, 3), (2, 6), (2, 7),
(3, 4), (3, 5), (3, 6), (3, 8),
(4, 5),
(5, 6), (5, 8),
(6, 7), (6, 8),
]
)
return g
def main() -> int:
g = build_graph()
levels = nx.single_source_shortest_path_length(g, 7)
is_planar, embedding = nx.check_planarity(g)
if not is_planar:
raise RuntimeError("counterexample graph should be planar")
pos = nx.planar_layout(g, scale=3.4)
fig, ax = plt.subplots(figsize=(8.6, 7.2))
nx.draw_networkx_edges(g, pos, ax=ax, edge_color="#cbd5e1", width=1.4)
cycle_edges = [(3, 4), (4, 5), (5, 8), (8, 3)]
nx.draw_networkx_edges(
g,
pos,
ax=ax,
edgelist=cycle_edges,
edge_color="#dc2626",
width=2.8,
)
level_colors = {0: "#0f172a", 1: "#475569", 2: "#94a3b8"}
for v, (x, y) in pos.items():
lev = levels[v]
ax.scatter(
[x],
[y],
s=600 if v == 7 else 500,
color=level_colors[lev],
edgecolors="black",
linewidths=1.1,
zorder=3,
)
ax.text(
x,
y,
f"{v}\n$\\ell={lev}$",
ha="center",
va="center",
color="white",
fontsize=10,
fontweight="bold",
zorder=4,
)
legend = [
Line2D(
[0],
[0],
marker="o",
color="w",
label=r"source $S=\{7\}$",
markerfacecolor=level_colors[0],
markeredgecolor="black",
markersize=12,
),
Line2D(
[0],
[0],
marker="o",
color="w",
label=r"level $L_1$",
markerfacecolor=level_colors[1],
markeredgecolor="black",
markersize=12,
),
Line2D(
[0],
[0],
marker="o",
color="w",
label=r"level $L_2$",
markerfacecolor=level_colors[2],
markeredgecolor="black",
markersize=12,
),
Line2D([0], [0], color="#dc2626", lw=2.8, label=r"problem cycle $(3,4,5,8)$"),
]
ax.legend(handles=legend, loc="lower center", ncol=2, framealpha=0.95)
ax.set_title(
"Counterexample to the universal-source form",
fontsize=14,
pad=18,
)
ax.set_aspect("equal")
ax.axis("off")
fig.tight_layout(rect=[0, 0.05, 1, 1])
out = os.path.join(OUT_DIR, "fig_universal_level_cycle_counterexample.png")
fig.savefig(out, dpi=180, bbox_inches="tight")
plt.close(fig)
print(f"wrote {out}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,189 @@
"""Generate and test candidate graph families for the level-cycle conjecture.
This script builds a few rigid maximal planar families and then runs the
existing level-cycle checker on them. It is meant as a small Sage-side
driver for probing likely counterexample sources before moving on to larger
families.
Available families:
stacked-ring concentric triangular rings like the paper's G_1
face-stack a chain of stacked triangulations built by repeatedly
stacking a vertex into a triangular face
Examples:
sage -python experiments/generate_candidate_families.py stacked-ring 1 6
sage -python experiments/generate_candidate_families.py face-stack 4 12 --full
"""
from __future__ import annotations
import argparse
import logging
from typing import Callable
from sage.all import Graph # type: ignore[attr-defined] # pylint: disable=no-name-in-module
from check_level_cycle_three_color import level_sources, test_graph
LOGGER = logging.getLogger(__name__)
def build_stacked_ring(levels: int) -> Graph:
"""Return the concentric triangular-ring family used in the paper figure."""
if levels < 1:
raise ValueError("stacked-ring requires at least one ring")
g = Graph()
g.add_vertex(0)
next_vertex = 1
ring1 = [next_vertex + i for i in range(3)]
next_vertex += 3
g.add_vertices(ring1)
g.add_edges([(0, v) for v in ring1])
g.add_edges([(ring1[i], ring1[(i + 1) % 3]) for i in range(3)])
inner = ring1
for _ in range(1, levels):
outer = [next_vertex + i for i in range(3)]
next_vertex += 3
g.add_vertices(outer)
g.add_edges([(outer[i], outer[(i + 1) % 3]) for i in range(3)])
g.add_edges([(inner[i], outer[i]) for i in range(3)])
g.add_edges([(inner[i], outer[(i + 1) % 3]) for i in range(3)])
inner = outer
expected_edges = 3 * g.n_vertices() - 6
if g.num_edges() != expected_edges:
raise AssertionError(
f"stacked-ring is not maximal planar: |V|={g.n_vertices()}, "
f"|E|={g.num_edges()}, expected {expected_edges}"
)
return g
def build_face_stack(steps: int) -> Graph:
"""Return a stacked triangulation built by repeatedly subdividing one face."""
if steps < 1:
raise ValueError("face-stack requires at least one step")
g = Graph()
g.add_vertices([0, 1, 2, 3])
g.add_edges([(0, 1), (1, 2), (2, 0)])
g.add_edges([(3, 0), (3, 1), (3, 2)])
active_face = (0, 1, 3)
next_vertex = 4
for _ in range(steps - 1):
v = next_vertex
next_vertex += 1
a, b, c = active_face
g.add_vertex(v)
g.add_edges([(v, a), (v, b), (v, c)])
active_face = (a, c, v)
expected_edges = 3 * g.n_vertices() - 6
if g.num_edges() != expected_edges:
raise AssertionError(
f"face-stack is not maximal planar: |V|={g.n_vertices()}, "
f"|E|={g.num_edges()}, expected {expected_edges}"
)
return g
FAMILY_BUILDERS: dict[str, Callable[[int], Graph]] = {
"stacked-ring": build_stacked_ring,
"face-stack": build_face_stack,
}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("family", choices=sorted(FAMILY_BUILDERS))
parser.add_argument("n_min", type=int, nargs="?", default=1)
parser.add_argument("n_max", type=int, nargs="?", default=6)
parser.add_argument(
"--sources",
choices=("vertex", "cycle", "all"),
default="vertex",
help="candidate source family to search",
)
parser.add_argument(
"--quantifier",
choices=("exists-source", "all-sources"),
default="exists-source",
help="whether to test the weakened or stronger conjecture",
)
parser.add_argument(
"--max-cycle-source-size",
type=int,
default=None,
help="optional cap on induced cycle source size",
)
parser.add_argument(
"--max-colorings",
type=int,
default=None,
help="optional cap per graph/source; capped searches report UNKNOWN",
)
parser.add_argument(
"--full",
action="store_true",
help="continue after failures/unknowns instead of stopping at first",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
builder = FAMILY_BUILDERS[args.family]
stop_first = not args.full
total_graphs = 0
total_sources = 0
unknown = 0
for n in range(args.n_min, args.n_max + 1):
g = builder(n)
total_graphs += 1
print(
f"{args.family} n={n}: "
f"|V|={g.n_vertices()} |E|={g.num_edges()} "
f"sources={args.sources} quantifier={args.quantifier}"
)
sources = list(
level_sources(g, args.sources, args.max_cycle_source_size)
)
passed, complete, checked_sources = test_graph(
g,
sources,
args.max_colorings,
stop_first,
args.quantifier,
)
total_sources += checked_sources
if not complete:
unknown += 1
if not passed and not args.full:
print(f"ABORT: first failure at family={args.family} n={n}")
print(
f"SUMMARY: checked {total_graphs} graphs and {total_sources} "
f"sources; unknown_graphs={unknown}"
)
return 1
print(
f"SUMMARY: checked {total_graphs} graphs and {total_sources} "
f"sources; unknown_graphs={unknown}"
)
return 0
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
try:
raise SystemExit(main())
except Exception as exc: # pragma: no cover - surfaced to the shell
LOGGER.exception("candidate family generation failed: %s", exc)
raise
Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.
@@ -1298,6 +1298,15 @@ level source. Then $G$ admits a proper $4$-vertex-colouring with the
level-cycle three-colour restriction with respect to $S$.
\end{conjecture}
\begin{figure}[htbp]
\centering
\includegraphics[width=0.78\textwidth]{fig_universal_level_cycle_counterexample.png}
\caption{The $8$-vertex counterexample to the universal-source form.
With source $S=\{7\}$, the level cycle $(3,4,5,8)$ lies in $L_2$ and
forces all four colours in every proper $4$-vertex-colouring.}
\label{fig:universal-level-cycle-counterexample}
\end{figure}
\begin{example}[Counterexample to Conjecture~\ref{conj:false-universal-level-cycle-three-colour}]
\label{ex:universal-level-cycle-counterexample}
Let $G$ be the maximal planar graph on vertex set