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.
Metadata
Release files for aberth-hybrid 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| aberth_hybrid-0.1.0.tar.gz | 774.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| aberth_hybrid-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 813.3 kB
Release files / aberth_hybrid-0.1.0.tar.gz
| Download URL | aberth_hybrid-0.1.0.tar.gz |
|---|---|
| Size | 774.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d458566e8812f26b3a3ce4c4ed0abbbebb890e3df50b4bb13f25ebed3f9ca517
|
|
BLAKE2b-256 checksum How to use checksums |
f317bf4490cdd6654e51f38884d772f3af7b00665d78f59c1c93ab6b91d9bf40
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.7.1
|
Release files / aberth_hybrid-0.1.0-py3-none-any.whl
| Download URL | aberth_hybrid-0.1.0-py3-none-any.whl |
|---|---|
| Size | 38.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
49922f35fcc034946fc544d8cd471b9cff7621334c459f18a2f38ea053184b2c
|
|
BLAKE2b-256 checksum How to use checksums |
673bd8a9f1190f30d8e51ccd981e7f82b086953ae197461b188ac67371b94aeb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.7.1
|