Hybrid contour-integral + Ehrlich-Aberth root-finder for analytic and transcendental functions on unbounded regions.
Project description
aberth-hybrid
Hybrid contour-integral + Ehrlich-Aberth pipeline for finding all zeros of an analytic function (or all eigenvalues of a matrix pencil) in a region of the complex plane. Handles polynomials, transcendentals, bounded regions, unbounded strips, and non-normal matrix nonlinear eigenvalue problems (NEPs).
Reference implementation for the preprint "A Hybrid Contour-Integral / Ehrlich-Aberth Root-Finder for Analytic and Transcendental Functions on Unbounded Regions" (arXiv: tbd).
Highlights
- 2.2× faster and 58× tighter median residual than
skzeroson the Bowhay-Nakatsukasa-Zaid combustion DDE benchmark (Python vs Python, same 2024 Apple M-series laptop). Julia port is 2.6× faster thanskzeros. - 17× to 415× faster than
cxrootson scalar analytic root-finding in a disk. - The only tool that solves the Hayes DDE unbounded strip in a single call (all 86 Lambert-$W$ zeros in $|\mathrm{Im}(z)| < 265$, at machine precision).
- 10⁷× better separation resolution than the standard Hermitian Rayleigh polish on close-eigenvalue non-normal matrix pencils, from the biorthogonal block Aberth iteration.
- Passes the standard NLEVP benchmark suite at machine-precision backward error (12 canonical problems from polynomial to transcendental).
Install
pip install aberth-hybrid # once published to PyPI
# or, faster:
uv pip install aberth-hybrid
From source (for contributors):
git clone https://github.com/peppe-tringali/aberth-hybrid
cd aberth-hybrid
uv sync --extra dev
uv run pytest # 27 unit tests
Three canonical examples
Example 1: scalar analytic function in a bounded disk
import numpy as np
from aberth_hybrid import hybrid_solve
# Zeros of z + exp(-z) inside the disk |z| < 8
def f(z): return z + np.exp(-z)
def fp(z): return 1.0 - np.exp(-z)
result = hybrid_solve(f, fp, center=0+0j, radius=8.0, tol=1e-13)
print(f"{result.n_zeros} zeros recovered")
for z in sorted(result.roots, key=lambda z: z.imag):
print(f" {z:+.6f} |f(z)| = {abs(f(z)):.2e}")
Expected output:
4 zeros recovered
-2.062278-7.588631j |f(z)| = 9.93e-16
-0.318132-1.337236j |f(z)| = 2.29e-16
-0.318132+1.337236j |f(z)| = 2.29e-16
-2.062278+7.588631j |f(z)| = 9.93e-16
Example 2: unbounded strip (Hayes DDE)
import numpy as np
from aberth_hybrid import Window, solve_on_windows
def f(z): return z + np.exp(-z) # Hayes DDE characteristic function
def fp(z): return 1.0 - np.exp(-z)
# Cover a vertical strip along the imaginary axis with overlapping disks
windows = [Window(center=complex(-2.5, k * 8.8), radius=5.5)
for k in range(-30, 31)]
result = solve_on_windows(f, fp, windows, tol=1e-13)
print(f"{len(result.roots)} zeros in the strip |Im(z)| < 265")
Expected: 86 zeros, each at machine precision, all Lambert-$W$ branches.
Example 3: matrix nonlinear eigenvalue problem
import numpy as np
from aberth_hybrid.beyn import beyn_extract
from aberth_hybrid.block_aberth import block_aberth_from_beyn
# Simple 3x3 matrix DDE: T(lam) = -lam*I + A + B*exp(-lam)
A = np.diag([-2.0, -3.0, -5.0]) + 0j
B = np.diag([1.5, 2.0, 1.2]) + 0j
def T(z):
return -complex(z) * np.eye(3, dtype=complex) + A + B * np.exp(-complex(z))
def T_prime(z):
return -np.eye(3, dtype=complex) - B * np.exp(-complex(z))
# Beyn extraction on a disk
beyn = beyn_extract(T, center=0+0j, radius=3.0, n_quad=1024,
n_probes=10, n_moments=10,
return_left_eigenvectors=True)
# Refine with prewarm + biorthogonal block Aberth
result = block_aberth_from_beyn(T, T_prime, beyn, tol=1e-14,
prewarm_steps=3)
print(f"{len(result.eigenvalues)} eigenvalues:")
for lam in sorted(result.eigenvalues, key=lambda z: (z.real, z.imag)):
print(f" {lam:+.6f}")
For a walkthrough of the theory (7-layer pipeline, prewarm theorem,
certificate), see the companion blog series.
For a fuller worked notebook, open docs/getting_started.ipynb.
Public API
Top-level (from aberth_hybrid import ...):
| Symbol | Purpose |
|---|---|
hybrid_solve(f, fp, center, radius, ...) |
Full pipeline on one disk |
Window(center, radius) |
Disk descriptor for windowing |
solve_on_windows(f, fp, windows, ...) |
Pipeline on multiple disks with dedup |
solve_adaptive(f, fp, root_window, ...) |
Adaptive 2×2 subdivision |
aberth(f, fp, initial_guesses, ...) |
Bare Ehrlich-Aberth iteration |
zero_count_circle(f, fp, center, radius, n_quad) |
Argument principle count |
hankel_recover_circle_svd(...) |
SVD-projected Hankel bootstrap |
Matrix NEPs (from aberth_hybrid.beyn import ... and
from aberth_hybrid.block_aberth import ...):
| Symbol | Purpose |
|---|---|
beyn_extract(T, center, radius, ...) |
Beyn contour extraction with block moments |
block_aberth(T, Tp, lam, V, W, ...) |
Biorthogonal block Aberth iteration |
block_aberth_from_beyn(T, Tp, beyn_result, ...) |
Composed extract + polish |
prewarm_eigenvectors(T, lam, V, W, steps) |
Fixed-shift two-sided inverse iteration |
prewarm_certificate(T, Tp, lam, V, W) |
A-posteriori check of Theorem 4.11 |
Parameter continuation (from aberth_hybrid.continuation import ...):
| Symbol | Purpose |
|---|---|
continuation_sweep(T, Tp, params, contour, ...) |
Certificate-gated warm-start sweep |
matrix_pencil_zero_count(T, Tp, center, radius, n_quad) |
Trace-form count |
Julia port
A performance-focused Julia translation lives in julia_port/. It
matches the Python implementation's algorithm and runs the combustion
DDE benchmark in 9.8 ms (median of 5 runs), 17% faster than the
Python reference. To run:
cd julia_port
julia --project=. test/runtests.jl # 12 unit tests
julia --project=. benchmarks/combustion_dde.jl # combustion benchmark
Benchmarks
Reproducible benchmarks under benchmarks/:
uv run python -m benchmarks.nlevp.run_all # NLEVP suite
uv run python -m benchmarks.head_to_head.scalar_vs_cxroots # vs cxroots
uv run python -m benchmarks.head_to_head.matrix_vs_neppack # vs NEP-PACK
uv run python -m benchmarks.head_to_head.combustion_vs_skzeros # vs skzeros
uv run python -m benchmarks.head_to_head.vs_big_math_libraries # vs numpy/scipy
bash benchmarks/run_full_suite.sh # everything
Or in Julia:
julia --project=julia_port julia_port/benchmarks/combustion_dde.jl
Tests
uv run pytest -q # 27 unit tests, all pass
uv run ruff check . # lint clean
Citation
If you use aberth-hybrid in your work, please cite:
@misc{TringaliAberthHybrid2026,
author = {Tringali, Giuseppe},
title = {A Hybrid Contour-Integral / {Ehrlich--Aberth} Root-Finder
for Analytic and Transcendental Functions on Unbounded
Regions},
year = {2026},
eprint = {tbd},
archivePrefix= {arXiv},
primaryClass = {math.NA}
}
License
MIT.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aberth_hybrid-0.1.0.tar.gz.
File metadata
- Download URL: aberth_hybrid-0.1.0.tar.gz
- Upload date:
- Size: 774.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.7.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d458566e8812f26b3a3ce4c4ed0abbbebb890e3df50b4bb13f25ebed3f9ca517
|
|
| MD5 |
09c546939f48df83ece620eef674c531
|
|
| BLAKE2b-256 |
f317bf4490cdd6654e51f38884d772f3af7b00665d78f59c1c93ab6b91d9bf40
|
File details
Details for the file aberth_hybrid-0.1.0-py3-none-any.whl.
File metadata
- Download URL: aberth_hybrid-0.1.0-py3-none-any.whl
- Upload date:
- Size: 38.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.7.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
49922f35fcc034946fc544d8cd471b9cff7621334c459f18a2f38ea053184b2c
|
|
| MD5 |
cffc22c7685ef6bcfc150bbb21ec3444
|
|
| BLAKE2b-256 |
673bd8a9f1190f30d8e51ccd981e7f82b086953ae197461b188ac67371b94aeb
|