Draw each full medial tire cut from the dual-cut experiment
Add draw_tire_cuts_png (and a --tire-png flag): one panel per recognised tread showing the annular cycle, up/down/bite teeth, walk-depth labels, and cut slits, ported from medial_tire_cut_labelling.to_tikz. Render the function-D (seed 7) graph's tire cuts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -304,6 +304,130 @@ def draw_png(result, path, scale=6.0):
|
|||||||
plt.close(fig)
|
plt.close(fig)
|
||||||
|
|
||||||
|
|
||||||
|
def _tire_coords(g, r_ann=1.0, r_up=1.46, r_down=0.60):
|
||||||
|
"""Annular/teeth coordinates for one tread, matching
|
||||||
|
``medial_tire_cut_labelling.to_tikz``: a_0 at the top, k increasing CW."""
|
||||||
|
import math
|
||||||
|
n = g.n
|
||||||
|
|
||||||
|
def ang(k):
|
||||||
|
return math.radians(90.0 - k * 360.0 / n)
|
||||||
|
|
||||||
|
def mid(i):
|
||||||
|
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 g.up_edges:
|
||||||
|
a = mid(i)
|
||||||
|
pos[f"u{i}"] = (r_up * math.cos(a), r_up * math.sin(a))
|
||||||
|
for i in g.singleton_down_edges:
|
||||||
|
a = mid(i)
|
||||||
|
pos[f"d{i}"] = (r_down * math.cos(a), r_down * math.sin(a))
|
||||||
|
for (i, j) in g.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 _draw_tread(ax, g, depth, cuts, entry_edge, title):
|
||||||
|
"""Draw one full medial tire cut on ``ax`` (annular cycle, teeth, walk-depth
|
||||||
|
labels, cut slits), mirroring ``medial_tire_cut_labelling.to_tikz``."""
|
||||||
|
import math
|
||||||
|
n = g.n
|
||||||
|
pos = _tire_coords(g)
|
||||||
|
|
||||||
|
def seg(a, b, **kw):
|
||||||
|
ax.plot([pos[a][0], pos[b][0]], [pos[a][1], pos[b][1]], **kw)
|
||||||
|
|
||||||
|
# annular cycle
|
||||||
|
xs = [pos[f"a{k}"][0] for k in range(n)] + [pos["a0"][0]]
|
||||||
|
ys = [pos[f"a{k}"][1] for k in range(n)] + [pos["a0"][1]]
|
||||||
|
ax.plot(xs, ys, color="black", lw=1.4, zorder=1)
|
||||||
|
# spokes (teeth)
|
||||||
|
for i in g.up_edges:
|
||||||
|
seg(f"u{i}", f"a{i}", color="0.55", lw=0.6, zorder=1)
|
||||||
|
seg(f"u{i}", f"a{(i + 1) % n}", color="0.55", lw=0.6, zorder=1)
|
||||||
|
for i in g.singleton_down_edges:
|
||||||
|
seg(f"d{i}", f"a{i}", color="0.55", lw=0.6, zorder=1)
|
||||||
|
seg(f"d{i}", f"a{(i + 1) % n}", color="0.55", lw=0.6, zorder=1)
|
||||||
|
for (i, j) in g.bites:
|
||||||
|
apex = f"p{i}_{j}"
|
||||||
|
for e in (i, j):
|
||||||
|
seg(apex, f"a{e}", color="0.55", lw=0.6, zorder=1)
|
||||||
|
seg(apex, f"a{(e + 1) % n}", color="0.55", lw=0.6, zorder=1)
|
||||||
|
# vertices
|
||||||
|
for k in range(n):
|
||||||
|
ax.plot(*pos[f"a{k}"], "o", ms=3, color="black", zorder=3)
|
||||||
|
for i in g.up_edges:
|
||||||
|
ax.plot(*pos[f"u{i}"], "o", ms=5, mfc="#cfe0f3", mec="#3a6ea5", zorder=3)
|
||||||
|
for i in g.singleton_down_edges:
|
||||||
|
ax.plot(*pos[f"d{i}"], "o", ms=5, mfc="#f3cfcf", mec="#a53a3a", zorder=3)
|
||||||
|
for (i, j) in g.bites:
|
||||||
|
ax.plot(*pos[f"p{i}_{j}"], "o", ms=6, mfc="#e8a0a0", mec="#a53a3a", zorder=3)
|
||||||
|
# walk-depth labels along each spoke
|
||||||
|
if depth is not None:
|
||||||
|
for edge in range(n):
|
||||||
|
ax_, ay = pos[g.apex_of_edge(edge)]
|
||||||
|
a, b = pos[f"a{edge}"], pos[f"a{(edge + 1) % n}"]
|
||||||
|
mx, my = 0.5 * (a[0] + b[0]), 0.5 * (a[1] + b[1])
|
||||||
|
lx, ly = ax_ + 0.5 * (mx - ax_), ay + 0.5 * (my - ay)
|
||||||
|
ax.text(lx, ly, str(depth[edge]), fontsize=7, fontweight="bold",
|
||||||
|
ha="center", va="center", zorder=4)
|
||||||
|
# cut slits
|
||||||
|
for c in cuts or []:
|
||||||
|
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)
|
||||||
|
ax.plot([vx - dx, vx + dx], [vy - dy, vy + dy],
|
||||||
|
color="#cc2020", lw=2.0, zorder=5)
|
||||||
|
ax.text(vx + 0.34 * math.cos(rad), vy + 0.34 * math.sin(rad),
|
||||||
|
f"cut {c.order + 1}", fontsize=6, color="#cc2020",
|
||||||
|
ha="center", va="center", zorder=5)
|
||||||
|
# entry marker
|
||||||
|
if entry_edge is not None:
|
||||||
|
ex, ey = pos[g.apex_of_edge(entry_edge)]
|
||||||
|
rad = math.atan2(ey, ex)
|
||||||
|
ax.text(ex + 0.34 * math.cos(rad), ey + 0.34 * math.sin(rad),
|
||||||
|
"entry", fontsize=6, color="#3a6ea5", ha="center", va="center")
|
||||||
|
ax.set_title(title, fontsize=8)
|
||||||
|
ax.set_aspect("equal")
|
||||||
|
ax.axis("off")
|
||||||
|
|
||||||
|
|
||||||
|
def draw_tire_cuts_png(result, path):
|
||||||
|
"""Render every recognised tread's full medial tire cut, one panel each."""
|
||||||
|
import matplotlib
|
||||||
|
matplotlib.use("Agg")
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
res = result["results"]
|
||||||
|
depths = sorted(res)
|
||||||
|
if not depths:
|
||||||
|
raise ValueError("no recognised treads to draw")
|
||||||
|
fig, axes = plt.subplots(1, len(depths), figsize=(5.2 * len(depths), 5.4))
|
||||||
|
if len(depths) == 1:
|
||||||
|
axes = [axes]
|
||||||
|
for ax, d in zip(axes, depths):
|
||||||
|
rec = res[d]
|
||||||
|
g = rec["g"]
|
||||||
|
title = (f"tread {d}: |A(T)|={g.n} word={g.tooth_word}\n"
|
||||||
|
f"bites={sorted(g.bites)} entry=e{rec['entry_edge']} "
|
||||||
|
f"start_depth={rec['start_depth']} cuts={len(rec['cuts'])}")
|
||||||
|
_draw_tread(ax, g, rec["depth"], rec["cuts"], rec["entry_edge"], title)
|
||||||
|
fig.suptitle(f"full medial tire cuts -- source {result['source']}, "
|
||||||
|
f"root entry e{result['entry_edge']}", fontsize=10)
|
||||||
|
fig.tight_layout()
|
||||||
|
fig.savefig(path, dpi=150)
|
||||||
|
plt.close(fig)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
@@ -315,6 +439,8 @@ def main():
|
|||||||
parser.add_argument("--entry", type=int, default=None,
|
parser.add_argument("--entry", type=int, default=None,
|
||||||
help="fix the root entry tooth (requires --source)")
|
help="fix the root entry tooth (requires --source)")
|
||||||
parser.add_argument("--png", metavar="PATH", help="render the dual cut to PNG")
|
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")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
rng = random.Random(args.seed)
|
rng = random.Random(args.seed)
|
||||||
@@ -338,6 +464,9 @@ def main():
|
|||||||
if args.png:
|
if args.png:
|
||||||
draw_png(result, args.png)
|
draw_png(result, args.png)
|
||||||
print(f"wrote {args.png}")
|
print(f"wrote {args.png}")
|
||||||
|
if args.tire_png:
|
||||||
|
draw_tire_cuts_png(result, args.tire_png)
|
||||||
|
print(f"wrote {args.tire_png}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
Reference in New Issue
Block a user