Draw the whole medial graph with all tire cuts

Add a --whole mode to draw_medial_tire_cut.py that renders the entire
medial graph M(G) (the assembled cut graph), on a Kamada-Kawai layout,
with the recognised tires highlighted (black annular vertices, blue/red
teeth carrying walk depths, larger red bite apex) and the rest of M(G)
in grey. Add the resulting figure (Figure 3) and a describing paragraph
to the paper for the n=20 seed-72 example, via an \input-ed .tikz file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 00:07:00 -04:00
parent 9d7cb7644e
commit c64c720e5a
6 changed files with 336 additions and 30 deletions
@@ -2,23 +2,29 @@
Paper-graphics companion to ``run_medial_tire_cut_experiment.py``: it imports
``run_experiment`` from there, runs the pipeline on a random maximal planar
graph, and emits a TikZ ``tikzpicture`` (walk-depth labels + cut slits) for each
recognised full medial tire graph of the decomposition, using ``to_tikz`` from
``medial_tire_cut_labelling``.
graph, and emits TikZ. By default it draws one ``tikzpicture`` (walk-depth
labels + cut slits) per recognised full medial tire graph, using ``to_tikz``
from ``medial_tire_cut_labelling``. With ``--whole`` it instead draws the whole
medial graph M(G) with every tire's cuts applied, on a Kamada--Kawai layout, the
recognised tires highlighted and the rest of M(G) in grey.
This script only renders; the experiment itself draws nothing. Run with the
repo venv (networkx): ``.venv/bin/python``.
Example:
Examples:
.venv/bin/python draw_medial_tire_cut.py -n 20 --seed 72 > panels.tex
.venv/bin/python draw_medial_tire_cut.py -n 20 --seed 72 --whole > whole.tex
"""
from __future__ import annotations
import argparse
import math
import os
import sys
import networkx as nx
_HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, _HERE)
@@ -38,14 +44,102 @@ def tikz_panels(n: int, seed: int, scale: float = 1.6) -> tuple[dict, list[str]]
return result, panels
# --------------------------------------------------------------------------- #
# The whole medial graph: M(G) with all tire cuts applied.
# --------------------------------------------------------------------------- #
def _is_split(node) -> bool:
return isinstance(node, tuple) and len(node) == 3 and node[1] in ("A", "B")
def _medial_layout(H: nx.Graph) -> dict:
"""A Kamada--Kawai layout of the (planar) cut graph, normalised to the unit
box. The two copies of a cut vertex have different neighbours, so the layout
separates them automatically, showing the slit."""
pos = nx.kamada_kawai_layout(H)
xs = [p[0] for p in pos.values()]
ys = [p[1] for p in pos.values()]
cx, cy = 0.5 * (max(xs) + min(xs)), 0.5 * (max(ys) + min(ys))
span = max(max(xs) - min(xs), max(ys) - min(ys)) or 1.0
return {v: ((p[0] - cx) / span, (p[1] - cy) / span) for v, p in pos.items()}
def medial_tikz(result: dict, scale: float = 9.0) -> str:
"""A TikZ ``tikzpicture`` of the whole medial graph M(G) with every tire's
cuts applied. Tire teeth are coloured and carry their walk depth; annular
medial vertices are black; medial vertices outside any recognised tire are
grey; cut (split) vertices are drawn as separated copies."""
H = result["cut_graph"]
pos = _medial_layout(H)
# role of each medial vertex: annular / up / down / bite, and walk depth.
annular = set()
for d in sorted(result["results"]):
g, bij = result["results"][d]["g"], result["results"][d]["bij"]
annular.update(bij[f"a{k}"] for k in range(g.n))
apex = {r["apex"]: (r["role"], r["walk"]) for r in result["labels"]}
def edge_of(node):
return node[0] if _is_split(node) else node
L = []
A = L.append
A(f"\\begin{{tikzpicture}}[scale={scale},")
A(" med/.style={black!30, line width=0.3pt},")
A(" grey/.style={circle, draw=black!45, fill=black!8, inner sep=0.9pt},")
A(" ann/.style={circle, fill=black, inner sep=1.0pt},")
A(" cutv/.style={circle, draw=red!75!black, fill=red!12, inner sep=1.0pt},")
A(" upv/.style={circle, draw=blue!70!black, fill=blue!15, inner sep=1.3pt},")
A(" downv/.style={circle, draw=red!70!black, fill=red!15, inner sep=1.3pt},")
A(" bitev/.style={circle, draw=red!70!black, fill=red!35, inner sep=1.6pt},")
A(" dlbl/.style={font=\\tiny\\bfseries, text=black, inner sep=0.5pt}]")
def pt(node):
x, y = pos[node]
return f"({x:.3f},{y:.3f})"
for u, v in H.edges():
A(f"\\draw[med] {pt(u)}--{pt(v)};")
for node in H.nodes():
mv = edge_of(node)
if mv in apex:
role, _ = apex[mv]
style = {"up": "upv", "down": "downv", "bite": "bitev"}[role]
elif mv in annular:
style = "cutv" if _is_split(node) else "ann"
else:
style = "grey"
A(f"\\node[{style}] at {pt(node)} {{}};")
for node in H.nodes():
mv = edge_of(node)
if _is_split(node) or mv not in apex:
continue
x, y = pos[node]
A(f"\\node[dlbl] at ({x:.3f},{y:.3f}) [yshift=4.5pt] {{{apex[mv][1]}}};")
A("\\end{tikzpicture}")
return "\n".join(L)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("-n", type=int, default=20)
parser.add_argument("--seed", type=int, default=72)
parser.add_argument("--scale", type=float, default=1.6)
parser.add_argument("--whole", action="store_true",
help="draw the whole medial graph M(G) with all cuts, "
"instead of one panel per tread")
args = parser.parse_args()
if args.whole:
result = run_experiment(n=args.n, seed=args.seed)
treads = sorted(result["results"])
print(f"% whole medial graph: n={args.n} seed={args.seed} "
f"source={result['source']} recognised treads={treads} "
f"|M(G)|={result['M'].number_of_nodes()}")
print(medial_tikz(result, scale=args.scale if args.scale != 1.6 else 9.0))
return
result, panels = tikz_panels(args.n, args.seed, scale=args.scale)
treads = sorted(result["results"])
print(f"% medial tire cut: n={args.n} seed={args.seed} "