codexcoercive-bilinear-form-load-response-bound.py
coercive-bilinear-form-load-response-bound.py
"""Plot A.T u = f and its coercivity bound; Python 3.14 / matplotlib 3.10.7 / NumPy 2.3.5."""
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
def main():
plt.rcParams.update({"font.size": 11, "figure.facecolor": "white", "axes.facecolor": "white"})
matrix = np.array([[2., 1.], [-1., 3.]])
angle = np.linspace(0, 2*np.pi, 721)
loads = np.vstack((np.cos(angle), np.sin(angle)))
responses = np.linalg.solve(matrix.T, loads)
alpha = np.linalg.eigvalsh((matrix+matrix.T)/2)[0]
assert alpha == 2
assert np.max(np.linalg.norm(responses, axis=0)) <= 1/alpha
assert np.allclose(matrix.T @ responses, loads, atol=1e-14)
fig, (ellipse, growth) = plt.subplots(1, 2, figsize=(10.3, 4.7), layout="constrained")
ellipse.plot(np.cos(angle)/alpha, np.sin(angle)/alpha, "--", color="#b45309", lw=2, label=r"Bound: $\|u\|_2\leq 1/2$")
ellipse.fill(responses[0], responses[1], color="#dbeafe", alpha=1)
ellipse.plot(responses[0], responses[1], color="#1d4ed8", lw=2, label=r"$u=(A^T)^{-1}f$, $\|f\|_2=1$")
colors = ["#15803d", "#7e22ce"]
for i, color in enumerate(colors):
response = np.linalg.solve(matrix.T, np.eye(2)[:, i])
ellipse.scatter(*response, color=color, s=45, zorder=4)
ellipse.annotate(fr"$f=e_{i+1}$", response, xytext=(9, 5), textcoords="offset points", color=color)
ellipse.axhline(0, color="#9ca3af", lw=0.7); ellipse.axvline(0, color="#9ca3af", lw=0.7)
ellipse.set(xlabel=r"$u_1$", ylabel=r"$u_2$", aspect="equal", xlim=(-.62, .62), ylim=(-.62, .62), title="Unit-load responses inside the bound")
ellipse.legend(loc="upper center", fontsize=9)
magnitude = np.linspace(0, 4, 101)
growth.plot(magnitude, magnitude/alpha, "--", color="#b45309", lw=2, label=r"Coercivity bound: $t/2$")
largest = 1/np.linalg.svd(matrix, compute_uv=False)[-1]
growth.plot(magnitude, largest*magnitude, color="#1d4ed8", lw=2, label="Largest response over load directions")
for i, color in enumerate(colors):
gain = np.linalg.norm(np.linalg.solve(matrix.T, np.eye(2)[:, i]))
growth.plot(magnitude, gain*magnitude, color=color, lw=1.8, label=fr"Load $f=te_{i+1}$")
growth.set(xlabel=r"Load norm $t=\|f\|_2$", ylabel=r"Response norm $\|u\|_2$", xlim=(0, 4), ylim=(0, 2.1), title="Fixed form: response scales with load")
growth.grid(alpha=.2); growth.legend(loc="upper left", fontsize=9)
fig.suptitle(r"$A=[[2,1],[-1,3]]$: solve $A^Tu=f$, with $\alpha=2$", fontsize=15)
fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=110, facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()