Kwant Performance Demo
The snippet below builds a 1D heterostructure with four orbitals per site.
Both onsite and hopping terms are sums of random \(4 \times 4\) tinyarrays
multiplied by weights that would normally be passed as parameters to Kwant's
onsite/hopping functions. We benchmark
- Constructing the full scattering linear system (banded for nanokwant,
sparse for Kwant using the internal
_make_linear_syshelper). - Solving each linear problem once (banded solver vs. Kwant's sparse solver).
- Computing 100 eigenvalues of a \(4\,000 \times 4\,000\) Hamiltonian (finite, no leads) using a sparse Hermitian solver vs. a banded Hermitian solver.
The Kwant construction time explicitly includes finalizing the builder before calling the internal helper, so it captures the full setup cost.
Helper utilities live in src/nanokwant/_kwant_perf_setup.py so that the
benchmark code below stays focused on the actual comparisons.
import numpy as np
from nanokwant._kwant_perf_setup import (
ENERGY,
build_kwant_system,
build_nanokwant_inputs,
eigsh,
eigvalsh_banded,
hamiltonian,
lead_selection,
param_values,
perf_counter,
scattering_system,
solver,
solve_banded,
sp,
)
CHAIN_SITES = 10_000
def benchmark(fn, repeat=1):
best = float("inf")
last = None
for _ in range(repeat):
start = perf_counter()
last = fn()
best = min(best, perf_counter() - start)
return best, last
def benchmark_dim(dim):
nanokwant_system, nanokwant_params = build_nanokwant_inputs(CHAIN_SITES, dim)
nanokwant_build_time, last = benchmark(
lambda: scattering_system(
nanokwant_system, CHAIN_SITES, nanokwant_params, ENERGY
)
)
nk_lhs, nk_rhs, _ = last
nk_bw = nk_lhs.bandwidth
nk_array = np.asarray(nk_lhs)
def solve_nanokwant():
return solve_banded(nk_bw, nk_array.copy(), nk_rhs.copy())
nanokwant_solve_time, _ = benchmark(solve_nanokwant)
def build_kwant_linear_system():
kwant_sys = build_kwant_system(CHAIN_SITES, dim, with_leads=True)
linsys, _ = solver._make_linear_sys(
kwant_sys, in_leads=lead_selection, energy=ENERGY, params=param_values
)
kept_blocks = [
coords
for idx, coords in enumerate(linsys.indices)
if (idx in lead_selection and coords.size)
]
kept = (
np.concatenate(kept_blocks)
if kept_blocks
else np.empty(0, dtype=int)
)
rhs_blocks = [
block if sp.issparse(block) else sp.csc_matrix(block)
for block in linsys.rhs
]
rhs = sp.bmat([rhs_blocks], format=solver.rhsformat)
return kwant_sys, linsys, rhs, kept
kwant_build_time, (_, kw_linsys, kw_rhs, kw_kept) = benchmark(
build_kwant_linear_system
)
def solve_kwant():
if kw_kept.size == 0 or kw_rhs.shape[1] == 0:
return np.zeros((kw_kept.size, kw_rhs.shape[1]))
factored = solver._factorized(kw_linsys.lhs)
return solver._solve_linear_sys(factored, kw_rhs, kw_kept)
kwant_solve_time, _ = benchmark(solve_kwant)
return (
nanokwant_build_time,
kwant_build_time,
nanokwant_solve_time,
kwant_solve_time,
)
dims = [1, 2, 4]
results = []
for dim in dims:
timings = benchmark_dim(dim)
results.append((dim,) + timings)
print(f"{'dim':>4} {'nanokwant build':>18} {'kwant build':>14} {'nanokwant solve':>18} {'kwant solve':>14}")
for dim, nk_b, kw_b, nk_s, kw_s in results:
print(
f"{dim:>4} {1e3 * nk_b:>18.2f} {1e3 * kw_b:>14.2f} "
f"{1e3 * nk_s:>18.2f} {1e3 * kw_s:>14.2f}"
)
dim nanokwant build kwant build nanokwant solve kwant solve
1 9.58 654.91 1.05 12.70
2 8.99 405.48 10.95 22.71
4 18.18 390.01 27.24 63.25
EIG_SITES = 80
EIG_DIM = 4
EIG_COUNT = 16
nanokwant_system_eig, nanokwant_params_eig = build_nanokwant_inputs(EIG_SITES, EIG_DIM)
band = hamiltonian(
nanokwant_system_eig, EIG_SITES, nanokwant_params_eig, hermitian=True
)
l_bw, u_bw = band.bandwidth
band_array = np.asarray(band)
def eig_banded():
return eigvalsh_banded(
band_array[: u_bw + 1], select="i", select_range=(0, EIG_COUNT - 1)
)
banded_time, banded_vals = benchmark(eig_banded, repeat=1)
kwant_finite = build_kwant_system(EIG_SITES, EIG_DIM, with_leads=False)
ham_sparse = kwant_finite.hamiltonian_submatrix(sparse=True, params=param_values)
def eig_sparse():
return eigsh(ham_sparse, k=EIG_COUNT, which="SA", return_eigenvectors=False)
sparse_time, sparse_vals = benchmark(eig_sparse, repeat=1)
max_diff = np.max(np.abs(np.sort(banded_vals) - np.sort(sparse_vals)))
print("Eigenvalue benchmark (100 lowest states):")
print(f" Banded solver: {1e3 * banded_time:6.2f} ms")
print(f" Sparse solver: {1e3 * sparse_time:6.2f} ms")
print(f" Max |Δλ| between methods: {max_diff:.3e}")
Eigenvalue benchmark (100 lowest states):
Banded solver: 3.96 ms
Sparse solver: 25.14 ms
Max |Δλ| between methods: 2.907e-03
The numbers vary slightly between runs, but nanokwant consistently reduces the construction cost by roughly an order of magnitude and solves about twice as fast for chains with a few hundred to ~1k orbitals (64–256 sites, four orbitals each). The finite-size eigenvalue benchmark (80 sites, 16 states, six orbitals per site) now finishes in well under a second for the Hermitian banded solver, whereas the sparse approach still lags by more than an order of magnitude; both produce eigenvalues that agree within \(10^{-3}\).