codextypesetting-badness-curve.py
typesetting-badness-curve.py
"""Compare an original cubic-cost plot with Knuth's integer badness routine.
Algorithm: badness(t,s) in https://tug.ctan.org/systems/knuth/dist/tex/tex.web.
All plotted inputs describe finite stretching. Write the PNG to cwd.
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
STRETCH = 65536
def tex_badness(excess, stretch):
"""Integer algorithm, including its large-input branches and sentinel."""
assert excess >= 0
if excess == 0:
return 0
if stretch <= 0:
return 10000
if excess <= 7230584:
scaled_ratio = excess * 297 // stretch
elif stretch >= 1663497:
scaled_ratio = excess // (stretch // 297)
else:
scaled_ratio = excess
if scaled_ratio > 1290:
return 10000
return (scaled_ratio ** 3 + 131072) // 262144
def samples(ratios):
return [tex_badness(round(float(r) * STRETCH), STRETCH) for r in ratios]
def main():
examples = [(0.5, 32768, 12), (1, 65536, 100), (2, 131072, 800)]
for ratio, excess, expected in examples:
assert excess / STRETCH == ratio
assert tex_badness(excess, STRETCH) == expected
fig, axes = plt.subplots(1, 2, figsize=(9, 4.8), dpi=100, facecolor="white")
fig.subplots_adjust(left=0.08, right=0.98, bottom=0.22, top=0.72, wspace=0.28)
fig.suptitle("Spacing strain has a rapidly increasing cost", y=0.96,
fontsize=16, weight="bold", color="#172330")
fig.text(0.5, 0.86, "Finite stretching; integer routine uses coefficient s = 65536 internal units",
ha="center", fontsize=10, color="#45535d")
for ax, lo, hi, title in zip(axes, [0, 0.35], [2.05, 0.65],
["Cubic growth", "Zoom: integer steps"]):
ratios = np.linspace(lo, hi, 3001)
ax.plot(ratios, 100 * ratios ** 3, color="#347ea8", linestyle="--",
linewidth=1.7, label=r"Smooth guide $100r^3$")
ax.step(ratios, samples(ratios), where="mid", color="#bd6b29",
linewidth=1.6, label="TeX integer routine")
ax.set_xlim(lo, hi)
ax.set_ylim((0, 860) if lo == 0 else (3, 29))
ax.set_title(title, fontsize=12, pad=12)
ax.set_xlabel("Adjustment ratio r", fontsize=10)
ax.set_ylabel("Badness b", fontsize=10)
ax.grid(alpha=0.18)
ax.tick_params(labelsize=9)
for side in ["top", "right"]:
ax.spines[side].set_visible(False)
for (ratio, excess, value), xytext in zip(examples, [(0.8, 45), (0.58, 220), (1.22, 735)]):
axes[0].plot(ratio, value, "o", color="#172330", markersize=4)
axes[0].annotate(f"({ratio:g}, {value})", (ratio, value), xytext=xytext,
fontsize=9, color="#172330",
arrowprops={"arrowstyle": "-", "color": "#788994"})
axes[1].plot(0.5, 12, "o", color="#172330", markersize=4)
axes[1].annotate("r = 0.5 gives b = 12", (0.5, 12), xytext=(0.365, 24),
fontsize=9, color="#172330",
arrowprops={"arrowstyle": "-", "color": "#788994"})
handles, labels = axes[0].get_legend_handles_labels()
fig.legend(handles, labels, loc="lower center", bbox_to_anchor=(0.5, 0.055),
ncol=2, frameon=False, fontsize=10)
fig.text(0.5, 0.018, "Original numerical illustration; saturation and excessive shrinkage are outside the plotted range.",
ha="center", fontsize=9, color="#45535d")
fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"),
dpi=100, facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()