Draw tread 0 (the source cap) in the dual-cut experiment

Add draw_cap_png and a --cap-png flag: render tread 0 as a wheel (source
hub, link-cycle rim, cap triangles filled, cap cut marked) from the
extract_tread roles, since tread 0 is skipped by tire recognition (a wheel
has no up teeth). Render funcD seed7's cap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 11:06:49 -04:00
parent 192d97a31d
commit 7554582056
2 changed files with 79 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

@@ -467,6 +467,80 @@ def draw_tire_cuts_png(result, path):
plt.close(fig)
def draw_cap_png(result, path):
"""Render tread 0, the source cap: a wheel with the source at the hub, its
link cycle as the rim, the cap triangles (down teeth) filled, and the cap
cut marked. Tread 0 is skipped by tire recognition (a wheel has no up
teeth), so this draws the ``extract_tread`` roles directly."""
import math
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
G, source = result["G"], result["source"]
faces, emb = triangular_faces(G)
levels = nx.single_source_shortest_path_length(G, source)
tr = extract_tread(faces, levels, 0)
if tr is None:
raise ValueError("no tread-0 (cap) faces")
link = list(emb.neighbors_cw_order(source))
cap_cuts = {c["medial_vertex"] for c in result.get("cap_cuts", [])}
pos = {source: (0.0, 0.0)}
k = len(link)
for i, v in enumerate(link):
a = math.radians(90 - i * 360.0 / k)
pos[v] = (math.cos(a), math.sin(a))
fig, ax = plt.subplots(figsize=(6.5, 6.8))
for f in tr["tread_faces"]:
if all(v in pos for v in f):
xy = [pos[v] for v in f]
ax.fill([p[0] for p in xy], [p[1] for p in xy],
color="#eef3fa", zorder=0)
def edge(u, v, **kw):
ax.plot([pos[u][0], pos[v][0]], [pos[u][1], pos[v][1]], **kw)
for u, v in tr["annular"]: # spokes (source -> link)
edge(u, v, color="0.45", lw=1.0, zorder=1)
for u, v in tr["down"]: # link cycle (down-tooth bases)
edge(u, v, color="black", lw=1.6, zorder=1)
ax.plot(*pos[source], "o", ms=11, mfc="#cfe0f3", mec="#3a6ea5", zorder=4)
ax.text(*pos[source], str(source), ha="center", va="center", fontsize=9,
fontweight="bold", color="#234", zorder=5)
for v in link:
ax.plot(*pos[v], "o", ms=9, mfc="white", mec="black", zorder=4)
x, y = pos[v]
ax.text(x * 1.13, y * 1.13, str(v), ha="center", va="center", fontsize=9)
for u, v in list(tr["annular"]) + list(tr["down"]):
mx, my = (pos[u][0] + pos[v][0]) / 2, (pos[u][1] + pos[v][1]) / 2
cut = ekey(u, v) in cap_cuts
ax.plot(mx, my, "s", ms=5, mfc=("#cc2020" if cut else "#888"),
mec="none", zorder=3)
if cut:
dx, dy = pos[v][0] - pos[u][0], pos[v][1] - pos[u][1]
L = math.hypot(dx, dy) or 1.0
px, py = -dy / L * 0.13, dx / L * 0.13
ax.plot([mx - px, mx + px], [my - py, my + py],
color="#cc2020", lw=2.2, zorder=5)
ax.text(mx + 0.12, my, "cap cut", color="#cc2020", fontsize=7,
va="center")
ax.set_title(f"tread 0 (source cap) -- source {source}, link {link}\n"
f"{len(tr['tread_faces'])} cap triangles; no up teeth (skipped); "
f"down teeth = link cycle", fontsize=8)
ax.set_aspect("equal")
ax.axis("off")
ax.set_xlim(-1.4, 1.4)
ax.set_ylim(-1.4, 1.4)
fig.tight_layout()
fig.savefig(path, dpi=150)
plt.close(fig)
def main():
parser = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
@@ -480,6 +554,8 @@ def main():
parser.add_argument("--png", metavar="PATH", help="render the dual cut to PNG")
parser.add_argument("--tire-png", metavar="PATH",
help="render each full medial tire cut to PNG")
parser.add_argument("--cap-png", metavar="PATH",
help="render tread 0 (the source cap) to PNG")
args = parser.parse_args()
rng = random.Random(args.seed)
@@ -506,6 +582,9 @@ def main():
if args.tire_png:
draw_tire_cuts_png(result, args.tire_png)
print(f"wrote {args.tire_png}")
if args.cap_png:
draw_cap_png(result, args.cap_png)
print(f"wrote {args.cap_png}")
if __name__ == "__main__":