Skip to content

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

  1. Constructing the full scattering linear system (banded for nanokwant, sparse for Kwant using the internal _make_linear_sys helper).
  2. Solving each linear problem once (banded solver vs. Kwant's sparse solver).
  3. 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}\).