codexfenchel-young-quadratic-gap.py
fenchel-young-quadratic-gap.py
"""Draw a quadratic Fenchel-Young gap; save basename.png to cwd.
Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
def main():
p = 2.0
sample = -1.0
x = np.linspace(-2.0, 3.8, 1001)
f = x**2 / 2
support = p * x - p**2 / 2
gap = f + p**2 / 2 - p * x
assert np.allclose(gap, (x - p)**2 / 2)
assert np.allclose(gap, f - support)
assert np.all(gap >= -1e-12)
sample_f = sample**2 / 2
sample_support = p * sample - p**2 / 2
sample_gap = sample_f - sample_support
assert sample_f == 0.5 and sample_support == -4 and sample_gap == 4.5
assert p**2 / 2 - (p * p - p**2 / 2) == 0
plt.rcParams.update({"font.size": 11})
fig, (ax, residual) = plt.subplots(
1, 2, figsize=(9.1, 4.5), layout="constrained", facecolor="white"
)
ax.plot(x, f, color="#244e78", lw=2.8, label=r"$f(x)=x^2/2$")
ax.plot(x, support, color="#cc6b20", lw=2.1,
label=r"$\ell_2(x)=2x-2$")
ax.annotate("", (sample, sample_f), (sample, sample_support),
arrowprops={"arrowstyle": "<->", "color": "#a53656", "lw": 2.2})
ax.scatter([sample, sample, p], [sample_f, sample_support, p**2 / 2],
color=["#244e78", "#cc6b20", "#287d70"], s=48, zorder=5)
ax.text(-0.8, -2.2, "$G_f(-1,2)=9/2$\nVertical distance = 4.5",
color="#a53656", fontsize=10,
bbox={"facecolor": "white", "edgecolor": "none", "pad": 3})
ax.annotate("Equality at x = p = 2", (p, p**2 / 2),
xytext=(0.0, 4.4), fontsize=10,
arrowprops={"arrowstyle": "->", "color": "#287d70"},
bbox={"facecolor": "white", "edgecolor": "none", "pad": 2})
ax.set(xlim=(-2, 3.8), ylim=(-6.7, 7.6), xlabel="Input x",
ylabel="Function / line value", title="Fix the slope p = 2")
ax.legend(loc="upper left", fontsize=10, framealpha=1)
residual.plot(x, gap, color="#a53656", lw=2.8,
label=r"$G_f(x,2)=(x-2)^2/2$")
residual.plot([sample, sample], [0, sample_gap],
color="#a53656", ls="--", lw=1.2)
residual.scatter([sample, p], [sample_gap, 0],
color=["#a53656", "#287d70"], s=48, zorder=5)
residual.annotate("(-1, 4.5)", (sample, sample_gap),
xytext=(12, 10), textcoords="offset points", color="#a53656")
residual.annotate("(2, 0): exact match", (p, 0),
xytext=(-82, 18), textcoords="offset points", color="#287d70")
residual.set(xlim=(-2, 3.8), ylim=(-0.45, 8.7), xlabel="Input x",
ylabel="Gap", title="Subtract the line from the function")
residual.legend(loc="upper right", fontsize=10, framealpha=1)
for panel in (ax, residual):
panel.set_facecolor("white")
panel.axhline(0, color="#b4bcc3", lw=0.7, zorder=0)
panel.axvline(0, color="#b4bcc3", lw=0.7, zorder=0)
panel.grid(color="#e5e8eb", lw=0.7)
panel.spines[["top", "right"]].set_visible(False)
fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"),
dpi=120, facecolor="white", transparent=False)
plt.close(fig)
print("Verified gap identities on 1001 inputs; G(-1,2)=4.5, G(2,2)=0.")
if __name__ == "__main__":
main()