c64c720e5a
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>
158 lines
6.4 KiB
Python
158 lines
6.4 KiB
Python
"""Draw the walk-depth labelling and cut of a medial tire decomposition.
|
|
|
|
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 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``.
|
|
|
|
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)
|
|
|
|
from run_medial_tire_cut_experiment import run_experiment # noqa: E402
|
|
from medial_tire_cut_labelling import to_tikz # noqa: E402
|
|
|
|
|
|
def tikz_panels(n: int, seed: int, scale: float = 1.6) -> tuple[dict, list[str]]:
|
|
"""Run the experiment and return ``(result, panels)``, one TikZ panel per
|
|
recognised tread, each showing that tread's walk-depth labelling and cut."""
|
|
result = run_experiment(n=n, seed=seed)
|
|
panels = []
|
|
for d in sorted(result["results"]):
|
|
rec = result["results"][d]
|
|
panels.append(to_tikz(rec["g"], depth=rec["depth"], cuts=rec["cuts"],
|
|
entry_edge=rec["entry_edge"], scale=scale))
|
|
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} "
|
|
f"source={result['source']} recognised treads={treads}")
|
|
if not panels:
|
|
print("% (no recognised full medial tire graphs for this graph)")
|
|
for d, panel in zip(treads, panels):
|
|
g = result["results"][d]["g"]
|
|
print(f"% --- tread {d}: |A(T)|={g.n} word={g.tooth_word} "
|
|
f"bites={sorted(g.bites)} ---")
|
|
print(panel)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|