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() launches scipy.optimize.fsolve independently 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 RootResult objects.

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

Three-panel figure showing Jenkins-Traub roots on complex plane, real zeros of a transcendental function, and complex zeros on the unit circle

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