Skip to content

Vogl-Hjalmarson-Dow Tight-Binding (material-specific parameters)

sp3s* tight-binding Hamiltonian builder using material-specific fitted parameters -- far more accurate than harrison_tb's universal table, at the cost of needing a separately fitted parameter row per material instead of one table for everything.

Source

P. Vogl, H. P. Hjalmarson, J. D. Dow, "A Semi-empirical tight-binding theory of the electronic structure of semiconductors", J. Phys. Chem. Solids 44 (5), 365-378 (1983). Parameter values (MATERIALS) are transcribed from an independent open-source implementation, github.com/rpmuller/TightBinding (TB.py), which cites the same paper. 16 zinc-blende/diamond semiconductors included: C, Si, Ge, Sn, SiC, AlP, AlAs, AlSb, GaP, GaAs, GaSb, InP, InAs, InSb, ZnSe, ZnTe.

The method

Same Slater-Koster sp3 machinery as harrison_tb, plus one addition: an extra s* orbital per atom (5 orbitals/atom instead of 4, 10x10 Hamiltonian per unit cell instead of 8x8). s* has no literal physical meaning -- it exists purely as a fitting degree of freedom to pull the lowest conduction band down to the right energy, something a bare sp3 basis structurally cannot do (see harrison_tb's accuracy notes: it routinely misplaces the conduction-band minimum for indirect-gap materials). Each element's Material row (Esa/Epa/Esc/Epc/Essa/Essc + 7 hopping integrals Vss/Vxx/Vxy/Vsapc/Vscpa/Vssapc/Vsscpa) is fitted as a whole per material, not derived from a universal formula the way harrison_tb.ETA is.

sp3s_star_hamiltonian(kxyz, material) builds the periodic Bloch Hamiltonian directly (k in units of \(2\pi/a\) along the conventional cubic axes: \(\Gamma=(0,0,0)\), \(X=(1,0,0)\), \(L=(0.5,0.5,0.5)\)). direct_gap_at_gamma reads off the gap at \(\Gamma\) -- only the true fundamental gap for direct-gap materials. For indirect-gap materials (Si, Ge), the real conduction-band minimum sits off-\(\Gamma\), so band_extrema_along_path(material, k_start, k_end) scans a k-space line and finds the true valence-band max / conduction-band min instead of reading only \(\Gamma\).

Accuracy

Validated against real experimental gaps (GaAs, Si, Ge) -- see the Dense-Evolution-Discovery validation page for the full writeup and numbers.

vhd_tb

Vogl-Hjalmarson-Dow (1983) material-specific sp3s* tight-binding parameters -- a more accurate alternative to harrison_tb's universal (materials-independent) parameters, at the cost of needing a separately fitted parameter set per material.

Source: P. Vogl, H. P. Hjalmarson, J. D. Dow, "A Semi-empirical tight-binding theory of the electronic structure of semiconductors", J. Phys. Chem. Solids 44 (5), 365-378 (1983). Parameter values below are transcribed from an independent open-source implementation of the same table, github.com/rpmuller/TightBinding (TB.py, fetched 2026-08-05), which cites the same paper; not reproduced from the paper itself (not open access). Only materials appearing in that source are included.

sp3s basis: s, px, py, pz, and an extra excited s orbital per atom (5 orbitals/atom, 10x10 Hamiltonian per zinc-blende/diamond 2-atom cell) -- the s* orbital exists purely to push the lowest conduction band down to the right energy without needing d-orbitals; it has no literal physical excited state behind it.

Hermiticity note: the reference implementation's own H-builder forms the lower-left (cation-anion) block as Hac.conjugate() without transposing it, which does not in general produce a Hermitian matrix (the block mixes Vsapc/Vscpa asymmetrically). _cation_anion_block below is built the same way as that source's get_Hac, but assembled into the full Hamiltonian with a proper conjugate transpose (.conj().T) -- verified Hermitian for every material in MATERIALS (see the module's own smoke test).

Validated against experiment (run 2026-08-05): GaAs direct gap at Gamma (k=0) computes to 1.55 eV vs. the experimental 1.42 eV (~9% high) -- compare to harrison_tb.zincblende_hamiltonian's 2.91 eV (~105% high) for the same material using Harrison's universal parameters. The improvement is expected: these parameters are fitted per material specifically to reproduce band-edge energies, unlike Harrison's one-table-fits-all approach.

sp3s_star_hamiltonian

sp3s_star_hamiltonian(kxyz, material)

10x10 sp3s Bloch Hamiltonian, basis order [anion: s,px,py,pz,s, cation: s,px,py,pz,s*].

material: a Material namedtuple, or a key into MATERIALS. kxyz: crystal momentum in units of 2*pi/a along the conventional cubic axes (Gamma=(0,0,0), X=(1,0,0), L=(0.5,0.5,0.5)).

Source code in dense_evolution/vhd_tb.py
def sp3s_star_hamiltonian(kxyz, material):
    """
    10x10 sp3s* Bloch Hamiltonian, basis order
    [anion: s,px,py,pz,s*, cation: s,px,py,pz,s*].

    material: a Material namedtuple, or a key into MATERIALS.
    kxyz: crystal momentum in units of 2*pi/a along the conventional
    cubic axes (Gamma=(0,0,0), X=(1,0,0), L=(0.5,0.5,0.5)).
    """
    if isinstance(material, str):
        if material not in MATERIALS:
            raise ValueError(f"no VHD parameters for {material!r}; available: {sorted(MATERIALS)}")
        material = MATERIALS[material]

    Ha = np.diag([material.Esa, material.Epa, material.Epa, material.Epa, material.Essa])
    Hc = np.diag([material.Esc, material.Epc, material.Epc, material.Epc, material.Essc])
    Hac = _cation_anion_block(material, kxyz)

    H = np.zeros((10, 10), dtype=np.complex128)
    H[0:5, 0:5] = Ha
    H[5:10, 5:10] = Hc
    H[0:5, 5:10] = Hac
    H[5:10, 0:5] = Hac.conj().T
    return H

direct_gap_at_gamma

direct_gap_at_gamma(material)

Conduction-band-minimum minus valence-band-maximum at k=Gamma, for the 8 valence electrons (4 lowest of the 8 sp3-derived bands filled; the 2 s bands are always empty conduction states in this model). Only meaningful as the* fundamental gap for direct-gap materials (e.g. GaAs) -- for indirect-gap materials (e.g. Si) this is the Gamma-Gamma transition, not the true (lower) indirect gap, which occurs off Gamma and this function does not compute.

Source code in dense_evolution/vhd_tb.py
def direct_gap_at_gamma(material):
    """Conduction-band-minimum minus valence-band-maximum at k=Gamma,
    for the 8 valence electrons (4 lowest of the 8 sp3-derived bands
    filled; the 2 s* bands are always empty conduction states in this
    model). Only meaningful as *the* fundamental gap for direct-gap
    materials (e.g. GaAs) -- for indirect-gap materials (e.g. Si) this
    is the Gamma-Gamma transition, not the true (lower) indirect gap,
    which occurs off Gamma and this function does not compute."""
    H = sp3s_star_hamiltonian((0.0, 0.0, 0.0), material)
    eig = np.sort(np.linalg.eigvalsh(H).real)
    return eig[4] - eig[3]

band_extrema_along_path

band_extrema_along_path(
    material, k_start, k_end, n_points=501
)

Scans the valence-band maximum and conduction-band minimum (bands 3 and 4 of the 10, 0-indexed, sorted) along the straight line from k_start to k_end (both in the same 2*pi/a cubic-axis units as sp3s_star_hamiltonian), and returns (vbm, vbm_k, cbm, cbm_k, gap) where vbm_k/cbm_k are the k-points (same units) where each extremum was found.

Needed for indirect-gap materials (e.g. Si, Ge): the true fundamental gap is not at Gamma, so direct_gap_at_gamma alone gives the wrong (too-large) number for them. Example: Si's conduction band minimum sits off-Gamma along Gamma->X (the Delta line), not at Gamma itself.

Source code in dense_evolution/vhd_tb.py
def band_extrema_along_path(material, k_start, k_end, n_points=501):
    """
    Scans the valence-band maximum and conduction-band minimum (bands 3
    and 4 of the 10, 0-indexed, sorted) along the straight line from
    k_start to k_end (both in the same 2*pi/a cubic-axis units as
    sp3s_star_hamiltonian), and returns
    (vbm, vbm_k, cbm, cbm_k, gap) where vbm_k/cbm_k are the k-points
    (same units) where each extremum was found.

    Needed for indirect-gap materials (e.g. Si, Ge): the true
    fundamental gap is not at Gamma, so direct_gap_at_gamma alone gives
    the wrong (too-large) number for them. Example: Si's conduction
    band minimum sits off-Gamma along Gamma->X (the Delta line), not
    at Gamma itself.
    """
    if isinstance(material, str):
        if material not in MATERIALS:
            raise ValueError(f"no VHD parameters for {material!r}; available: {sorted(MATERIALS)}")
        material = MATERIALS[material]

    k_start = np.asarray(k_start, dtype=float)
    k_end = np.asarray(k_end, dtype=float)
    t = np.linspace(0.0, 1.0, n_points)
    ks = k_start[None, :] + t[:, None] * (k_end - k_start)[None, :]

    vbm_per_k = np.empty(n_points)
    cbm_per_k = np.empty(n_points)
    for i, k in enumerate(ks):
        eig = np.sort(np.linalg.eigvalsh(sp3s_star_hamiltonian(k, material)).real)
        vbm_per_k[i] = eig[3]
        cbm_per_k[i] = eig[4]

    vbm_idx = vbm_per_k.argmax()
    cbm_idx = cbm_per_k.argmin()
    vbm, vbm_k = vbm_per_k[vbm_idx], ks[vbm_idx]
    cbm, cbm_k = cbm_per_k[cbm_idx], ks[cbm_idx]
    return vbm, vbm_k, cbm, cbm_k, cbm - vbm

See also: harrison_tb for the zero-per-material-fitting universal alternative, when the ~2-3x gap error is an acceptable tradeoff for not needing a fitted parameter row per material.