codexgeometric-power-series-approximation.py
geometric-power-series-approximation.py
"""Compare geometric partial sums with exact, uniform truncation errors.
Run from the desired output directory. All curves follow finite identities;
the plot is not a fitted or sampled approximation.
"""
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
def partial_sum(x, degree):
x = np.asarray(x)
result = np.ones_like(x, dtype=float)
term = np.ones_like(x, dtype=float)
for _ in range(degree):
term = term * x
result = result + term
return result
def minimum_degree(radius, tolerance):
# Test the bound directly to avoid a rounding error at exact integer logs.
degree = 0
while radius ** (degree + 1) / (1 - radius) > tolerance:
degree += 1
return degree
def main():
plt.rcParams.update({"font.size": 11, "axes.titlesize": 13})
fig, axes = plt.subplots(1, 2, figsize=(11.8, 5.7), dpi=100)
x = np.linspace(-0.95, 0.95, 1401)
axes[0].plot(x, 1 / (1 - x), color="#333333", linewidth=2.5,
label="Exact 1/(1 − x)")
for degree, color in [(2, "#27835f"), (5, "#7755a3"), (15, "#c47724"), (40, "#2867a0")]:
axes[0].plot(x, partial_sum(x, degree), color=color, linewidth=1.8,
linestyle="--", label=f"Degree N = {degree}")
axes[0].set_title("Finite polynomials approximate an infinite sum")
axes[0].set_xlabel("Real x, strictly inside |x| < 1")
axes[0].set_ylabel("Partial sum / function value")
axes[0].set_xlim(-0.95, 0.95)
axes[0].set_ylim(0, 21)
axes[0].legend(loc="upper left", fontsize=9.5)
degrees = np.arange(0, 1201)
tolerance = 0.001
for radius, color in [(0.5, "#27835f"), (0.9, "#c47724"), (0.99, "#2867a0")]:
errors = radius ** (degrees + 1) / (1 - radius)
mask = errors > 1e-7
axes[1].semilogy(degrees[mask], errors[mask], color=color,
linewidth=2.3, label=f"Closed disk |z| ≤ {radius:g}")
needed = minimum_degree(radius, tolerance)
actual = radius ** (needed + 1) / (1 - radius)
axes[1].scatter([needed], [actual], color=color, s=35, zorder=4)
text_xy = {0.5: (100, 0.018), 0.9: (400, 0.0035), 0.99: (825, 0.020)}
axes[1].annotate(f"N = {needed}", xy=(needed, actual),
xytext=text_xy[radius], color=color, fontsize=10,
arrowprops={"arrowstyle": "->", "color": color})
axes[1].axhline(tolerance, color="#666666", linestyle=":", linewidth=1.4)
axes[1].text(620, tolerance * 0.65, "Tolerance 10⁻³", fontsize=10, color="#444444")
axes[1].set_title("Near the boundary, more terms may be needed")
axes[1].set_xlabel("Polynomial degree N")
axes[1].set_ylabel("Exact worst-case error rᴺ⁺¹ / (1 − r)")
axes[1].set_xlim(0, 1200)
axes[1].set_ylim(1e-5, 200)
axes[1].legend(loc="upper right", fontsize=9.5)
for ax in axes:
ax.grid(alpha=0.18)
fig.suptitle("A radius of convergence gives a domain; a remainder gives a stopping rule",
fontsize=15, y=0.975)
fig.text(0.5, 0.035,
"Degrees count from the constant term. "
"The error bound is attained at the positive real point z = r.",
ha="center", fontsize=10)
fig.subplots_adjust(left=0.075, right=0.975, top=0.82, bottom=0.19, wspace=0.28)
fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"),
facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()