Zeros-Search Examples — ZPORC, ZREAL, and ZANLY¶
Three complementary zero-finding techniques are demonstrated in a single script, covering polynomial, real, and complex function classes.
- Part A — Jenkins-Traub (ZPORC)
nonlinearequations.polynomial_roots_jenkins_traub()finds all roots of \((x-1)^5 = x^5 - 5x^4 + 10x^3 - 10x^2 + 5x - 1\). Coefficients are supplied in descending degree order (IMSL ZPORC convention). All five roots cluster at \(x = 1\) (multiplicity 5).- Part B — Multiple real zeros (ZREAL)
nonlinearequations.zeros_real()launchesscipy.optimize.fsolveindependently from each of 40 uniformly spaced starting points on \([0,\, 2\pi]\) to locate all real zeros of\[f(x) = \sin(3x)\cos(x) - 0.5\]Unique converged roots are extracted from the resulting list of
RootResultobjects.- Part C — Complex analytic zeros (ZANLY)
nonlinearequations.zeros_complex_analytic()uses the argument principle (contour integration) to count and then refine all zeros of \(f(z) = z^3 - 1\) inside the disk \(|z| \le 1.5\). The three cube-roots of unity are recovered to near machine precision.
Example Code¶
"""IMSL zeros-search examples: ZPORC, ZREAL, and ZANLY.
Three techniques for finding zeros are demonstrated in a single script:
**Part A — Jenkins-Traub polynomial roots (ZPORC)**
Finds all roots of the quintic :math:`(x-1)^5 = x^5 - 5x^4 + 10x^3 - 10x^2 + 5x - 1`
using :func:`nonlinearequations.polynomial_roots_jenkins_traub`.
Coefficients are given in descending degree order (IMSL/ZPORC convention).
**Part B — Multiple real zeros (ZREAL)**
Finds all real zeros of :math:`f(x) = \\sin(3x)\\cos(x) - 0.5` on
:math:`[0, 2\\pi]` using :func:`nonlinearequations.zeros_real` with
uniformly spaced starting points.
**Part C — Complex analytic zeros (ZANLY)**
Finds the three cube-roots of unity :math:`z^3 - 1 = 0` inside the
disk :math:`|z| \\le 1.5` using
:func:`nonlinearequations.zeros_complex_analytic`.
Outputs:
- All roots printed to stdout.
- Three-panel SVG figure saved to
``test_output/example_imsl_zeros_search.svg``.
"""
from __future__ import annotations
from pathlib import Path
from typing import Dict
import matplotlib.pyplot as plt
import numpy as np
from nonlinearequations import (
polynomial_roots_jenkins_traub,
zeros_real,
zeros_complex_analytic,
)
def run_demo_imsl_zeros_search() -> Dict[str, object]:
"""Run all three zeros-search demos and produce a combined 3-panel figure.
Part A uses :func:`polynomial_roots_jenkins_traub` (ZPORC) on a
quintic polynomial. Part B uses :func:`zeros_real` (ZREAL) on a
transcendental function. Part C uses :func:`zeros_complex_analytic`
(ZANLY) on a complex function.
Args:
None
Returns:
Dict[str, object]: Keys ``roots_jt`` (RootResult), ``zeros_r``
(list[RootResult]), ``zeros_c`` (RootResult), ``plot_path``
(str).
"""
# ====================================================================
# Part A — Jenkins-Traub: roots of (x-1)^5
# ====================================================================
# Coefficients in DESCENDING order: x^5 - 5x^4 + 10x^3 - 10x^2 + 5x - 1
coeffs_jt = np.array([1.0, -5.0, 10.0, -10.0, 5.0, -1.0])
result_jt = polynomial_roots_jenkins_traub(coeffs_jt)
print("\nPart A — Jenkins-Traub (ZPORC): roots of (x-1)^5")
print("-" * 55)
print(f" Polynomial: x^5 - 5x^4 + 10x^3 - 10x^2 + 5x - 1")
print(f" Expected: all roots = 1.0 (multiplicity 5)")
for i, r in enumerate(result_jt.x, 1):
print(f" Root {i}: {r.real:+.6f} {r.imag:+.6f}j")
print(f" Max |p(root)|: {result_jt.fval:.3e} converged: {result_jt.success}")
# ====================================================================
# Part B — zeros_real: f(x) = sin(3x)*cos(x) - 0.5 on [0, 2π]
# ====================================================================
def f_trig(x: float) -> float:
"""Transcendental target function.
Args:
x (float): Evaluation point.
Returns:
float: sin(3x)*cos(x) - 0.5.
"""
return np.sin(3.0 * x) * np.cos(x) - 0.5
# Dense starting-point grid to catch all sign changes in [0, 2π]
x_guesses = np.linspace(0.05, 2 * np.pi - 0.05, 40)
results_real = zeros_real(f_trig, x_guesses, x_tol=1e-10, max_fev=500)
# Collect unique converged zeros (fsolve may wander slightly outside [0,2π])
converged_zeros = sorted(
{round(r.x[0], 8) for r in results_real if r.success}
)
print("\nPart B — zeros_real (ZREAL): f(x) = sin(3x)*cos(x) - 0.5")
print("-" * 55)
print(f" Starting points supplied: {len(x_guesses)}")
print(f" Unique zeros found: {len(converged_zeros)}")
for z in converged_zeros:
print(f" x = {z:.6f} f(x) = {f_trig(z):.2e}")
# ====================================================================
# Part C — zeros_complex_analytic: z^3 - 1 = 0 inside |z| ≤ 1.5
# ====================================================================
def f_cube(z: complex) -> complex:
"""Cube-roots of unity target function.
Args:
z (complex): Evaluation point in the complex plane.
Returns:
complex: z^3 - 1.
"""
return z**3 - 1.0
result_cplx = zeros_complex_analytic(f_cube, center=0 + 0j, radius=1.5, n_points=200)
# Exact cube roots of unity for reference
exact_roots = np.array([np.exp(2j * np.pi * k / 3) for k in range(3)])
print("\nPart C — zeros_complex_analytic (ZANLY): z^3 - 1 = 0 inside |z| ≤ 1.5")
print("-" * 55)
print(f" Zeros found: {len(result_cplx.x)}")
for z in result_cplx.x:
print(f" z = {z.real:+.6f} {z.imag:+.6f}j |f(z)| = {abs(f_cube(z)):.2e}")
print(f" Max residual: {result_cplx.fval:.3e} converged: {result_cplx.success}")
# ====================================================================
# Three-panel figure
# ====================================================================
output_dir = Path("test_output")
output_dir.mkdir(parents=True, exist_ok=True)
plot_path = output_dir / "example_imsl_zeros_search.svg"
fig, axes = plt.subplots(1, 3, figsize=(16, 5))
# ------------------------------------------------------------------
# Panel A: polynomial roots on complex plane
# ------------------------------------------------------------------
ax_a = axes[0]
roots_a = result_jt.x
ax_a.scatter(roots_a.real, roots_a.imag, color="#0e7490", s=80, zorder=5,
label=f"JT roots ({len(roots_a)})")
ax_a.axhline(0, color="#aaaaaa", linewidth=0.7)
ax_a.axvline(0, color="#aaaaaa", linewidth=0.7)
ax_a.set_xlabel("Re(x)")
ax_a.set_ylabel("Im(x)")
ax_a.set_title("Part A — Jenkins-Traub\n$(x-1)^5$: all roots at 1")
ax_a.legend(fontsize=9)
ax_a.grid(True, alpha=0.25)
# Annotate expected position
ax_a.annotate("Expected: 1+0j", xy=(1.0, 0.0), xytext=(0.5, 0.4),
fontsize=8, color="#0e7490",
arrowprops={"arrowstyle": "->", "color": "#0e7490"})
# ------------------------------------------------------------------
# Panel B: zeros of transcendental function
# ------------------------------------------------------------------
ax_b = axes[1]
x_plot = np.linspace(0, 2 * np.pi, 600)
ax_b.plot(x_plot, f_trig(x_plot), color="#0891b2", linewidth=1.8,
label=r"$\sin(3x)\cos(x) - 0.5$")
ax_b.axhline(0, color="#aaaaaa", linewidth=0.7)
if converged_zeros:
z_arr = np.array(converged_zeros)
ax_b.scatter(z_arr, np.zeros_like(z_arr), color="#d62728", s=60, zorder=5,
label=f"Zeros ({len(converged_zeros)})")
ax_b.set_xlabel("x")
ax_b.set_ylabel("f(x)")
ax_b.set_title("Part B — zeros_real (ZREAL)\n" r"$\sin(3x)\cos(x) - 0.5 = 0$ on $[0, 2\pi]$")
ax_b.legend(fontsize=9)
ax_b.grid(True, alpha=0.25)
# ------------------------------------------------------------------
# Panel C: complex zeros on unit circle
# ------------------------------------------------------------------
ax_c = axes[2]
theta = np.linspace(0, 2 * np.pi, 300)
ax_c.plot(np.cos(theta), np.sin(theta), color="#aaaaaa", linewidth=1.0,
linestyle="--", label="Unit circle")
# Exact roots
ax_c.scatter(exact_roots.real, exact_roots.imag, color="#aaaaaa", s=120,
marker="x", linewidths=2.0, label="Exact roots", zorder=4)
# Found roots
if len(result_cplx.x) > 0:
cz = result_cplx.x
ax_c.scatter(cz.real, cz.imag, color="#0e7490", s=70, zorder=5,
label=f"ZANLY roots ({len(cz)})")
ax_c.axhline(0, color="#cccccc", linewidth=0.5)
ax_c.axvline(0, color="#cccccc", linewidth=0.5)
ax_c.set_aspect("equal")
ax_c.set_xlabel("Re(z)")
ax_c.set_ylabel("Im(z)")
ax_c.set_title("Part C — zeros_complex_analytic (ZANLY)\n$z^3 - 1 = 0$ inside $|z| \\leq 1.5$")
ax_c.legend(fontsize=9)
ax_c.grid(True, alpha=0.25)
fig.suptitle("Zeros-Search Examples: ZPORC — ZREAL — ZANLY", fontsize=13)
fig.subplots_adjust(left=0.06, right=0.98, bottom=0.12, top=0.88, wspace=0.32)
fig.savefig(plot_path, format="svg")
plt.close(fig)
return {
"roots_jt": result_jt,
"zeros_r": results_real,
"zeros_c": result_cplx,
"plot_path": str(plot_path),
}
if __name__ == "__main__":
run_demo_imsl_zeros_search()
Plot Output¶
Left: Jenkins-Traub roots of \((x-1)^5\) on the complex plane — all five roots tightly cluster at \(1 + 0j\). Centre: Function curve and zero locations for \(\sin(3x)\cos(x) - 0.5\). Right: Three cube-roots of unity found by contour integration, overlaid on the unit circle.¶
Console Output¶
Part A ΓÇö Jenkins-Traub (ZPORC): roots of (x-1)^5
-------------------------------------------------------
Polynomial: x^5 - 5x^4 + 10x^3 - 10x^2 + 5x - 1
Expected: all roots = 1.0 (multiplicity 5)
Root 1: +0.999646 +0.000902j
Root 2: +0.999033 -0.000059j
Root 3: +1.000747 +0.000616j
Root 4: +0.999757 -0.000938j
Root 5: +1.000817 -0.000520j
Max |p(root)|: 9.236e-16 converged: True
Part B ΓÇö zeros_real (ZREAL): f(x) = sin(3x)*cos(x) - 0.5
-------------------------------------------------------
Starting points supplied: 40
Unique zeros found: 7
x = 0.177617 f(x) = -1.30e-09
x = 0.785398 f(x) = 6.79e-09
x = 3.319210 f(x) = -1.01e-08
x = 3.926991 f(x) = -6.03e-09
x = 6.460803 f(x) = 5.62e-09
x = 7.068583 f(x) = 1.15e-09
x = 9.602395 f(x) = -3.18e-09
Part C ΓÇö zeros_complex_analytic (ZANLY): z^3 - 1 = 0 inside |z| Γëñ 1.5
-------------------------------------------------------
Zeros found: 3
z = +1.000000 +0.000000j |f(z)| = 0.00e+00
z = -0.500000 +0.866025j |f(z)| = 4.56e-15
z = -0.500000 -0.866025j |f(z)| = 3.27e-13
Max residual: 3.268e-13 converged: True