Add walk-depth labelling/cut script and worked example

New experiments/medial_tire_cut_labelling.py: takes a full medial tire
graph and an entry up tooth and runs the walk-depth labelling-and-cut
procedure, reusing the full medial tire generator's model and emitting
TikZ. Add a generator-produced 8-tooth example to the paper (Figure 1,
Example 2.3) showing the labelling and the two cuts, plus a remark
fixing the cut's closing tooth for descended faces.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 22:00:52 -04:00
parent 291f7e98c7
commit b4ddc7da8b
5 changed files with 566 additions and 23 deletions
@@ -0,0 +1,386 @@
"""Walk-depth labelling and cut of a full medial tire graph.
Implements the procedure of Definition 2.1 ("Walk-depth labelling and cut") of
the *Medial Tire Cuts* paper:
1. Pick an arbitrary up tooth, the entry tooth; it has walk depth d.
2. Traverse all teeth bounding the inner face incident to the entry tooth
clockwise until reaching the entry tooth, incrementing the walk depth by 1
for each tooth traversed.
3. On reaching the last tooth in the face, perform a cut by duplicating the
annular vertex at which the traversal closes (the annular vertex shared by
the last tooth and the closing tooth).
4. Find the tooth t of highest walk depth that is a member of a bite.
5. If t is incident to a face F with unlabelled teeth, traverse the teeth of F
starting from t in the direction of the unlabelled tooth incident to t
(sharing an annular vertex), incrementing the walk depth as you go.
6. Repeat steps 3-5 until all teeth are labelled.
The full medial tire graph model (annular cycle A(T), up/down teeth, bites, the
auxiliary plane graph B(T) and its inner faces) is the one from the companion
``full_medial_tire_generator.py`` of the medial tire decompositions paper, which
we import.
Teeth are identified with the annular edges that carry them: edge i sits on the
annular vertices a_i and a_{(i+1) mod n} and carries exactly one tooth. A bite
(i, j) carries two teeth, one on edge i and one on edge j, that share the bite
apex p. The inner non-tooth faces of B(T) are the root face (written ``None``)
and one inner-gap face per bite.
"""
from __future__ import annotations
import argparse
import math
import os
import sys
# Import the full medial tire model from the companion paper's experiments.
_GEN_DIR = os.path.normpath(os.path.join(
os.path.dirname(__file__), "..", "..",
"medial_tire_decompositions_of_plane_triangulations", "experiments",
))
sys.path.insert(0, _GEN_DIR)
from full_medial_tire_generator import ( # noqa: E402
FullMedialTireGraph,
has_incident_bite,
innermost_bite,
satisfies_bite_face_condition,
)
Face = "tuple[int, int] | None" # a bite (i, j), or None for the root face
# ---------------------------------------------------------------------------
# Face structure of B(T).
# ---------------------------------------------------------------------------
def parent_face(graph: FullMedialTireGraph, bite: tuple[int, int]) -> Face:
"""The face directly enclosing ``bite``: the minimal-span bite strictly
containing it, or the root face ``None``."""
i, j = bite
enclosing = [b for b in graph.bites if b[0] < i and b[1] > j]
if not enclosing:
return None
return min(enclosing, key=lambda b: b[1] - b[0])
def door_bite(graph: FullMedialTireGraph, edge: int) -> tuple[int, int] | None:
"""The bite that ``edge`` is a door of (i.e. a bite edge), or None."""
for b in graph.bites:
if edge in b:
return b
return None
def faces_bordered(graph: FullMedialTireGraph, edge: int) -> list[Face]:
"""The inner non-tooth faces whose boundary the tooth on ``edge`` lies on.
A bite door borders two faces (its bite's gap and that bite's parent); any
other tooth borders the single face directly containing its edge.
"""
bite = door_bite(graph, edge)
if bite is not None:
return [bite, parent_face(graph, bite)]
return [innermost_bite(edge, graph.bites)]
def face_boundary(graph: FullMedialTireGraph, face: Face) -> list[int]:
"""The teeth (annular edges) bounding ``face``, in clockwise cyclic order.
Clockwise is increasing edge index. For the root face the boundary is read
around the whole cycle; for a bite gap (i, j) it is read along the arc
i, i+1, ..., j and closes through the bite apex. Edges enclosed by a child
bite are skipped (they belong to the child's gap face).
"""
n = graph.n
arc = range(n) if face is None else range(face[0], face[1] + 1)
return [k for k in arc if face in faces_bordered(graph, k)]
def all_faces(graph: FullMedialTireGraph) -> list[Face]:
return [None] + sorted(graph.bites)
def shared_annular_vertex(graph: FullMedialTireGraph, e1: int, e2: int) -> int | None:
"""The annular vertex a_k shared by edges ``e1`` and ``e2``, or None."""
n = graph.n
common = {e1, (e1 + 1) % n} & {e2, (e2 + 1) % n}
return next(iter(common)) if common else None
# ---------------------------------------------------------------------------
# The walk-depth labelling and cut.
# ---------------------------------------------------------------------------
class Cut:
"""A cut performed when a face traversal closes: the duplicated annular
vertex, together with the last labelled tooth and the closing tooth that
share it, and the face being closed."""
__slots__ = ("vertex", "last_tooth", "closing_tooth", "face", "order")
def __init__(self, vertex, last_tooth, closing_tooth, face, order):
self.vertex = vertex
self.last_tooth = last_tooth
self.closing_tooth = closing_tooth
self.face = face
self.order = order
def __repr__(self):
f = "root" if self.face is None else f"bite{self.face}"
return (f"Cut(order={self.order}, a{self.vertex}, "
f"last=e{self.last_tooth}, closing=e{self.closing_tooth}, face={f})")
def label_and_cut(graph: FullMedialTireGraph, entry_edge: int,
start_depth: int = 0) -> tuple[dict[int, int], list[Cut]]:
"""Run the procedure starting from up tooth ``entry_edge``.
Returns ``(depth, cuts)`` where ``depth`` maps each annular edge (tooth) to
its walk depth, and ``cuts`` is the list of cuts in the order performed.
"""
if graph.tooth_word[entry_edge] != "U":
raise ValueError(f"entry edge {entry_edge} is not an up tooth")
depth: dict[int, int] = {}
cuts: list[Cut] = []
counter = start_depth
def traverse(face: Face, start_edge: int, is_entry: bool) -> None:
nonlocal counter
boundary = face_boundary(graph, face)
m = len(boundary)
pos = boundary.index(start_edge)
if is_entry:
depth[start_edge] = counter
counter += 1
direction = +1
else:
# head toward the unlabelled tooth incident to the door t
direction = +1 if boundary[(pos + 1) % m] not in depth else -1
last_new = start_edge
i = pos
while True:
i = (i + direction) % m
edge = boundary[i]
if edge in depth: # the closing tooth
cuts.append(Cut(
vertex=shared_annular_vertex(graph, last_new, edge),
last_tooth=last_new, closing_tooth=edge,
face=face, order=len(cuts),
))
return
depth[edge] = counter
counter += 1
last_new = edge
# Steps 1-3: the entry face.
traverse(innermost_bite(entry_edge, graph.bites), entry_edge, is_entry=True)
# Steps 4-6: descend through bites, deepest first.
while len(depth) < graph.n:
labelled_bite_teeth = sorted(
(e for e in depth if door_bite(graph, e) is not None),
key=lambda e: depth[e], reverse=True,
)
for t in labelled_bite_teeth:
target = next((F for F in faces_bordered(graph, t)
if any(e not in depth for e in face_boundary(graph, F))),
None)
if target is not None:
traverse(target, t, is_entry=False)
break
else:
break # no progress possible
return depth, cuts
# ---------------------------------------------------------------------------
# TikZ rendering.
# ---------------------------------------------------------------------------
def _coords(graph: FullMedialTireGraph,
r_ann=1.0, r_up=1.46, r_down=0.60) -> dict[str, tuple[float, float]]:
n = graph.n
def ang(k): # a_0 at the top, increasing k clockwise
return math.radians(90.0 - k * 360.0 / n)
def edge_mid_dir(i): # angle of the bisector of edge i's two endpoints
a0, a1 = ang(i), ang((i + 1) % n)
return math.atan2(math.sin(a0) + math.sin(a1), math.cos(a0) + math.cos(a1))
pos = {f"a{k}": (r_ann * math.cos(ang(k)), r_ann * math.sin(ang(k)))
for k in range(n)}
for i in graph.up_edges:
a = edge_mid_dir(i)
pos[f"u{i}"] = (r_up * math.cos(a), r_up * math.sin(a))
for i in graph.singleton_down_edges:
a = edge_mid_dir(i)
pos[f"d{i}"] = (r_down * math.cos(a), r_down * math.sin(a))
for (i, j) in graph.bites:
pts = [pos[f"a{i}"], pos[f"a{(i + 1) % n}"],
pos[f"a{j}"], pos[f"a{(j + 1) % n}"]]
cx = sum(p[0] for p in pts) / 4.0
cy = sum(p[1] for p in pts) / 4.0
pos[f"p{i}_{j}"] = (0.9 * cx, 0.9 * cy)
return pos
def _edge_midpoint(pos, graph, edge):
n = graph.n
a, b = pos[f"a{edge}"], pos[f"a{(edge + 1) % n}"]
return (0.5 * (a[0] + b[0]), 0.5 * (a[1] + b[1]))
def to_tikz(graph: FullMedialTireGraph,
depth: dict[int, int] | None = None,
cuts: list[Cut] | None = None,
entry_edge: int | None = None,
scale: float = 2.2) -> str:
"""A standalone ``tikzpicture`` for ``graph``; if ``depth`` is given, draw
the walk-depth labels and (with ``cuts``) the cut marks."""
pos = _coords(graph)
n = graph.n
L = []
A = L.append
A(f"\\begin{{tikzpicture}}[scale={scale},")
A(" ann/.style={circle, fill=black, inner sep=1.0pt},")
A(" upv/.style={circle, draw=blue!70!black, fill=blue!12, inner sep=1.4pt},")
A(" downv/.style={circle, draw=red!70!black, fill=red!12, inner sep=1.4pt},")
A(" bitev/.style={circle, draw=red!70!black, fill=red!32, inner sep=1.7pt},")
A(" cyc/.style={black, line width=1.0pt},")
A(" tth/.style={black!55, line width=0.4pt},")
A(" lbl/.style={font=\\scriptsize},")
A(" dlbl/.style={font=\\scriptsize\\bfseries, text=black},")
A(" cut/.style={red!80!black, line width=1.3pt},")
A(" cutlbl/.style={font=\\tiny, text=red!75!black}]")
def pt(name):
x, y = pos[name]
return f"({x:.3f},{y:.3f})"
# annular cycle
cyc = "--".join(pt(f"a{k}") for k in range(n)) + "--cycle"
A(f"\\draw[cyc] {cyc};")
# spokes
for i in graph.up_edges:
A(f"\\draw[tth] {pt(f'u{i}')}--{pt(f'a{i}')} {pt(f'u{i}')}--{pt(f'a{(i+1)%n}')};")
for i in graph.singleton_down_edges:
A(f"\\draw[tth] {pt(f'd{i}')}--{pt(f'a{i}')} {pt(f'd{i}')}--{pt(f'a{(i+1)%n}')};")
for (i, j) in graph.bites:
apex = f"p{i}_{j}"
for e in (i, j):
A(f"\\draw[tth] {pt(apex)}--{pt(f'a{e}')} {pt(apex)}--{pt(f'a{(e+1)%n}')};")
# vertices
for k in range(n):
A(f"\\node[ann] at {pt(f'a{k}')} {{}};")
for i in graph.up_edges:
A(f"\\node[upv] at {pt(f'u{i}')} {{}};")
for i in graph.singleton_down_edges:
A(f"\\node[downv] at {pt(f'd{i}')} {{}};")
for (i, j) in sorted(graph.bites):
A(f"\\node[bitev] at {pt(f'p{i}_{j}')} {{}};")
# walk-depth labels: placed along the spoke from apex toward the edge mid
if depth is not None:
for edge in range(n):
apex = graph.apex_of_edge(edge)
ax, ay = pos[apex]
mx, my = _edge_midpoint(pos, graph, edge)
f = 0.5
lx, ly = ax + f * (mx - ax), ay + f * (my - ay)
A(f"\\node[dlbl] at ({lx:.3f},{ly:.3f}) {{{depth[edge]}}};")
# cut marks: a short red slit across the duplicated annular vertex
if cuts:
for c in cuts:
if c.vertex is None:
continue
vx, vy = pos[f"a{c.vertex}"]
rad = math.atan2(vy, vx)
dx, dy = 0.16 * math.cos(rad), 0.16 * math.sin(rad)
A(f"\\draw[cut] ({vx-dx:.3f},{vy-dy:.3f})--({vx+dx:.3f},{vy+dy:.3f});")
lx, ly = vx + 0.30 * math.cos(rad), vy + 0.30 * math.sin(rad)
A(f"\\node[cutlbl] at ({lx:.3f},{ly:.3f}) {{cut {c.order+1}}};")
if entry_edge is not None:
ex, ey = pos[graph.apex_of_edge(entry_edge)]
rad = math.atan2(ey, ex)
tx, ty = ex + 0.34 * math.cos(rad), ey + 0.34 * math.sin(rad)
A(f"\\node[lbl, text=blue!60!black] at ({tx:.3f},{ty:.3f}) {{entry}};")
A("\\end{tikzpicture}")
return "\n".join(L)
# ---------------------------------------------------------------------------
# Worked example and CLI.
# ---------------------------------------------------------------------------
def worked_example() -> FullMedialTireGraph:
"""A clean 8-tooth piece: one bite (0,4), three down singletons 1,2,3 in its
gap, three up teeth 5,6,7 in the root face."""
return FullMedialTireGraph(n=8, tooth_word="DDDDDUUU", bites=frozenset({(0, 4)}))
def _check(graph: FullMedialTireGraph) -> None:
assert not has_incident_bite(graph.bites, graph.n), "bite uses incident edges"
assert satisfies_bite_face_condition(graph.tooth_word, graph.bites), \
"violates the bite-face condition"
assert graph.tooth_word.count("U") >= 3, "fewer than three up teeth"
def _describe(graph, depth, cuts) -> str:
lines = ["edge type walk-depth"]
for e in range(graph.n):
t = graph.tooth_word[e]
kind = {"U": "up"}.get(t, "down")
if door_bite(graph, e) is not None:
kind = "bite"
lines.append(f" e{e} {kind:<5} {depth[e]}")
lines.append("cuts (in order):")
for c in cuts:
f = "root" if c.face is None else f"bite{c.face}"
lines.append(f" cut {c.order+1}: duplicate a{c.vertex} "
f"(closing tooth e{c.closing_tooth} of {f})")
return "\n".join(lines)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--entry", default="u5",
help="entry up tooth, as an edge index or apex name like u5")
parser.add_argument("--start-depth", type=int, default=0)
parser.add_argument("--tikz", choices=["plain", "labelled", "both"],
help="emit TikZ for the worked example")
args = parser.parse_args()
entry = args.entry
edge = int(entry[1:]) if isinstance(entry, str) and entry.startswith("u") else int(entry)
graph = worked_example()
_check(graph)
depth, cuts = label_and_cut(graph, edge, start_depth=args.start_depth)
if args.tikz == "plain":
print(to_tikz(graph))
elif args.tikz == "labelled":
print(to_tikz(graph, depth=depth, cuts=cuts, entry_edge=edge))
elif args.tikz == "both":
print("% --- plain ---")
print(to_tikz(graph))
print("% --- labelled + cut ---")
print(to_tikz(graph, depth=depth, cuts=cuts, entry_edge=edge))
else:
print(f"worked example: n={graph.n} word={graph.tooth_word} "
f"bites={sorted(graph.bites)} entry=e{edge}")
print(_describe(graph, depth, cuts))
if __name__ == "__main__":
main()
+6 -1
View File
@@ -5,11 +5,16 @@
\@writefile{toc}{\contentsline {section}{\tocsection {}{1}{Introduction}}{1}{}\protected@file@percent }
\@writefile{toc}{\contentsline {section}{\tocsection {}{2}{Cutting a full medial tire graph}}{1}{}\protected@file@percent }
\newlabel{def:walk-depth-cut}{{2.1}{1}}
\citation{bauerfeld-medial-tire}
\bibcite{bauerfeld-medial-tire}{1}
\newlabel{tocindent-1}{0pt}
\newlabel{tocindent0}{12.7778pt}
\newlabel{tocindent1}{17.77782pt}
\newlabel{tocindent2}{0pt}
\newlabel{tocindent3}{0pt}
\newlabel{rem:closing-tooth}{{2.2}{2}}
\newlabel{ex:worked-cut}{{2.3}{2}}
\@writefile{toc}{\contentsline {section}{\tocsection {}{}{References}}{2}{}\protected@file@percent }
\gdef \@abspage@last{2}
\@writefile{lof}{\contentsline {figure}{\numberline {1}{\ignorespaces A full medial tire graph (left) and its walk-depth labelling and cut (right), from Example\nonbreakingspace 2.3\hbox {}. Black vertices are the annular medial vertices of the cycle $A(T)$; blue vertices are up-tooth apexes, red vertices are down-tooth apexes, and the larger red vertex is the shared apex of the bite on annular edges $0$ and $4$. On the right, each tooth carries its walk depth, and the two red slits mark the cuts: \emph {cut\nonbreakingspace 1} duplicates $a_5$ as the root-face traversal closes, and \emph {cut\nonbreakingspace 2} duplicates $a_1$ as the bite's inner-gap face closes. After the cuts the only bounded faces are the eight teeth.}}{3}{}\protected@file@percent }
\newlabel{fig:worked-cut}{{1}{3}}
\gdef \@abspage@last{3}
+28 -22
View File
@@ -1,4 +1,4 @@
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 14 JUN 2026 21:38
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 14 JUN 2026 21:56
entering extended mode
restricted \write18 enabled.
%&-line parsing enabled.
@@ -495,30 +495,36 @@ File: epstopdf-sys.cfg 2010/07/13 v1.3 Configuration of (r)epstopdf for TeX Liv
e
))
[1{/usr/local/texlive/2022/texmf-var/fonts/map/pdftex/updmap/pdftex.map}]
[2] (./paper.aux) )
LaTeX Warning: `h' float specifier changed to `ht'.
[2] [3] (./paper.aux) )
Here is how much of TeX's memory you used:
13526 strings out of 478268
270009 string characters out of 5846347
533560 words of memory out of 5000000
31357 multiletter control sequences out of 15000+600000
476571 words of font info for 56 fonts, out of 8000000 for 9000
13647 strings out of 478268
272595 string characters out of 5846347
554433 words of memory out of 5000000
31476 multiletter control sequences out of 15000+600000
477049 words of font info for 58 fonts, out of 8000000 for 9000
1302 hyphenation exceptions out of 8191
84i,6n,89p,414b,305s stack positions out of 10000i,1000n,20000p,200000b,200000s
</usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfo
nts/cm/cmbx10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfon
ts/cm/cmcsc10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfon
ts/cm/cmmi10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfont
s/cm/cmr10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/
cm/cmr7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/
cmr8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cms
s10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy
10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti1
0.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti8.
pfb>
Output written on paper.pdf (2 pages, 130843 bytes).
84i,9n,89p,936b,704s stack positions out of 10000i,1000n,20000p,200000b,200000s
</usr/local/texlive/2022/texmf-dist/fonts/type1/public/a
msfonts/cm/cmbx10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/am
sfonts/cm/cmbx7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsf
onts/cm/cmcsc10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsf
onts/cm/cmmi10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfo
nts/cm/cmr10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfont
s/cm/cmr6.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/c
m/cmr7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/c
mr8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmss
10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy1
0.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti10
.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti8.p
fb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmtt10.pf
b>
Output written on paper.pdf (3 pages, 172798 bytes).
PDF statistics:
64 PDF objects out of 1000 (max. 8388607)
39 compressed objects within 1 object stream
82 PDF objects out of 1000 (max. 8388607)
50 compressed objects within 1 object stream
0 named destinations out of 1000 (max. 500000)
13 words of extra memory for PDF output out of 10000 (max. 10000000)
Binary file not shown.
+146
View File
@@ -115,6 +115,152 @@ cuts as follows.
\end{enumerate}
\end{definition}
\begin{remark}[Closing tooth of a descended face]
\label{rem:closing-tooth}
For the entry face the traversal of step (2) closes by returning to the
entry tooth, so the cut of step (3) duplicates the annular vertex shared
by the last tooth and the entry tooth. For a face $F$ entered in step
(5), the traversal instead closes upon reaching an already-labelled
tooth: the other tooth of the bite through which $F$ was entered. In
both cases the cut of step (3) duplicates the annular vertex shared by
the last newly labelled tooth and this \emph{closing tooth}. Since both
teeth of a bite are labelled while traversing its parent face, every
descended face closes on such a tooth.
\end{remark}
\begin{example}[A worked walk-depth labelling and cut]
\label{ex:worked-cut}
Figure~\ref{fig:worked-cut} shows a full medial tire graph with annular
cycle of length $8$, generated by the full medial tire generator
of~\cite{bauerfeld-medial-tire}. Its eight teeth are: three up teeth on
the annular edges $5,6,7$ in the root face; one bite pairing the annular
edges $0$ and $4$; and three singleton down teeth on the annular edges
$1,2,3$ lying in that bite's inner-gap face.
Take the up tooth on edge $5$ as the entry tooth, with walk depth $0$.
Its inner face is the root face, bounded by the teeth on edges
$5,6,7,0,4$ in clockwise order. Step (2) labels them
\[
5\mapsto 0,\quad 6\mapsto 1,\quad 7\mapsto 2,\quad
0\mapsto 3,\quad 4\mapsto 4,
\]
and step (3) cuts by duplicating the annular vertex $a_5$ shared by the
last tooth (edge $4$) and the entry tooth (edge $5$). The highest-depth
bite tooth is now the one on edge $4$ (walk depth $4$); it is incident to
the still-unlabelled inner-gap face of the bite $(0,4)$. Entering that
face from edge $4$ toward its unlabelled neighbour, step (5) labels the
three down teeth
\[
3\mapsto 5,\quad 2\mapsto 6,\quad 1\mapsto 7,
\]
and closes on the already-labelled bite tooth of edge $0$, so step (3)
cuts by duplicating the annular vertex $a_1$
(Remark~\ref{rem:closing-tooth}). All eight teeth are now labelled, and
the two cuts leave only the outer face and the eight teeth as
$3$-faces. The labelling and cuts are produced by the script
\texttt{experiments/medial\_tire\_cut\_labelling.py}.
\end{example}
\begin{figure}[h]
\centering
\begin{tikzpicture}[scale=1.6,
ann/.style={circle, fill=black, inner sep=1.0pt},
upv/.style={circle, draw=blue!70!black, fill=blue!12, inner sep=1.4pt},
downv/.style={circle, draw=red!70!black, fill=red!12, inner sep=1.4pt},
bitev/.style={circle, draw=red!70!black, fill=red!32, inner sep=1.7pt},
cyc/.style={black, line width=1.0pt},
tth/.style={black!55, line width=0.4pt},
lbl/.style={font=\scriptsize},
dlbl/.style={font=\scriptsize\bfseries, text=black},
cut/.style={red!80!black, line width=1.3pt},
cutlbl/.style={font=\tiny, text=red!75!black}]
\draw[cyc] (0.000,1.000)--(0.707,0.707)--(1.000,0.000)--(0.707,-0.707)--(0.000,-1.000)--(-0.707,-0.707)--(-1.000,-0.000)--(-0.707,0.707)--cycle;
\draw[tth] (-1.349,-0.559)--(-0.707,-0.707) (-1.349,-0.559)--(-1.000,-0.000);
\draw[tth] (-1.349,0.559)--(-1.000,-0.000) (-1.349,0.559)--(-0.707,0.707);
\draw[tth] (-0.559,1.349)--(-0.707,0.707) (-0.559,1.349)--(0.000,1.000);
\draw[tth] (0.554,0.230)--(0.707,0.707) (0.554,0.230)--(1.000,0.000);
\draw[tth] (0.554,-0.230)--(1.000,0.000) (0.554,-0.230)--(0.707,-0.707);
\draw[tth] (0.230,-0.554)--(0.707,-0.707) (0.230,-0.554)--(0.000,-1.000);
\draw[tth] (0.000,-0.000)--(0.000,1.000) (0.000,-0.000)--(0.707,0.707);
\draw[tth] (0.000,-0.000)--(0.000,-1.000) (0.000,-0.000)--(-0.707,-0.707);
\node[ann] at (0.000,1.000) {};
\node[ann] at (0.707,0.707) {};
\node[ann] at (1.000,0.000) {};
\node[ann] at (0.707,-0.707) {};
\node[ann] at (0.000,-1.000) {};
\node[ann] at (-0.707,-0.707) {};
\node[ann] at (-1.000,-0.000) {};
\node[ann] at (-0.707,0.707) {};
\node[upv] at (-1.349,-0.559) {};
\node[upv] at (-1.349,0.559) {};
\node[upv] at (-0.559,1.349) {};
\node[downv] at (0.554,0.230) {};
\node[downv] at (0.554,-0.230) {};
\node[downv] at (0.230,-0.554) {};
\node[bitev] at (0.000,-0.000) {};
\end{tikzpicture}
\qquad
\begin{tikzpicture}[scale=1.6,
ann/.style={circle, fill=black, inner sep=1.0pt},
upv/.style={circle, draw=blue!70!black, fill=blue!12, inner sep=1.4pt},
downv/.style={circle, draw=red!70!black, fill=red!12, inner sep=1.4pt},
bitev/.style={circle, draw=red!70!black, fill=red!32, inner sep=1.7pt},
cyc/.style={black, line width=1.0pt},
tth/.style={black!55, line width=0.4pt},
lbl/.style={font=\scriptsize},
dlbl/.style={font=\scriptsize\bfseries, text=black},
cut/.style={red!80!black, line width=1.3pt},
cutlbl/.style={font=\tiny, text=red!75!black}]
\draw[cyc] (0.000,1.000)--(0.707,0.707)--(1.000,0.000)--(0.707,-0.707)--(0.000,-1.000)--(-0.707,-0.707)--(-1.000,-0.000)--(-0.707,0.707)--cycle;
\draw[tth] (-1.349,-0.559)--(-0.707,-0.707) (-1.349,-0.559)--(-1.000,-0.000);
\draw[tth] (-1.349,0.559)--(-1.000,-0.000) (-1.349,0.559)--(-0.707,0.707);
\draw[tth] (-0.559,1.349)--(-0.707,0.707) (-0.559,1.349)--(0.000,1.000);
\draw[tth] (0.554,0.230)--(0.707,0.707) (0.554,0.230)--(1.000,0.000);
\draw[tth] (0.554,-0.230)--(1.000,0.000) (0.554,-0.230)--(0.707,-0.707);
\draw[tth] (0.230,-0.554)--(0.707,-0.707) (0.230,-0.554)--(0.000,-1.000);
\draw[tth] (0.000,-0.000)--(0.000,1.000) (0.000,-0.000)--(0.707,0.707);
\draw[tth] (0.000,-0.000)--(0.000,-1.000) (0.000,-0.000)--(-0.707,-0.707);
\node[ann] at (0.000,1.000) {};
\node[ann] at (0.707,0.707) {};
\node[ann] at (1.000,0.000) {};
\node[ann] at (0.707,-0.707) {};
\node[ann] at (0.000,-1.000) {};
\node[ann] at (-0.707,-0.707) {};
\node[ann] at (-1.000,-0.000) {};
\node[ann] at (-0.707,0.707) {};
\node[upv] at (-1.349,-0.559) {};
\node[upv] at (-1.349,0.559) {};
\node[upv] at (-0.559,1.349) {};
\node[downv] at (0.554,0.230) {};
\node[downv] at (0.554,-0.230) {};
\node[downv] at (0.230,-0.554) {};
\node[bitev] at (0.000,-0.000) {};
\node[dlbl] at (0.177,0.427) {3};
\node[dlbl] at (0.704,0.292) {7};
\node[dlbl] at (0.704,-0.292) {6};
\node[dlbl] at (0.292,-0.704) {5};
\node[dlbl] at (-0.177,-0.427) {4};
\node[dlbl] at (-1.101,-0.456) {0};
\node[dlbl] at (-1.101,0.456) {1};
\node[dlbl] at (-0.456,1.101) {2};
\draw[cut] (-0.594,-0.594)--(-0.820,-0.820);
\node[cutlbl] at (-0.919,-0.919) {cut 1};
\draw[cut] (0.594,0.594)--(0.820,0.820);
\node[cutlbl] at (0.919,0.919) {cut 2};
\node[lbl, text=blue!60!black] at (-1.663,-0.689) {entry};
\end{tikzpicture}
\caption{A full medial tire graph (left) and its walk-depth labelling and
cut (right), from Example~\ref{ex:worked-cut}. Black vertices are the
annular medial vertices of the cycle $A(T)$; blue vertices are up-tooth
apexes, red vertices are down-tooth apexes, and the larger red vertex is
the shared apex of the bite on annular edges $0$ and $4$. On the right,
each tooth carries its walk depth, and the two red slits mark the cuts:
\emph{cut~1} duplicates $a_5$ as the root-face traversal closes, and
\emph{cut~2} duplicates $a_1$ as the bite's inner-gap face closes. After
the cuts the only bounded faces are the eight teeth.}
\label{fig:worked-cut}
\end{figure}
\begin{thebibliography}{9}
\bibitem{bauerfeld-medial-tire}