1D Chain Contacts in gauNEGF
This guide walks through how to set up an NEGF transport calculation using
energy-dependent 1D chain contacts via the NEGFE class and surfG surface
Green’s function. This method is ideal for periodic or quasi-periodic structures
where you can define semi-infinite leads as repeating 1D chains.
Warning
Contact-atom basis must be minimal. Atoms participating in the 1D contact slice must use a minimal basis (e.g. STO-3G for light atoms, LANL2DZ for heavy atoms). Polarized or diffuse functions on contact atoms produce a poorly-conditioned overlap matrix between adjacent unit cells, which breaks the surface Green’s function iteration. Interior (device) atoms can use any basis. See Best Practices for Production Calculations for the conditioning background.
Overview
The workflow has three stages:
Run a large DFT cluster calculation in Gaussian to get bulk-like matrices
Extract the Fock, overlap, and coupling matrices for the device and contacts
Set up the
NEGFEobject and run the self-consistent field (SCF) loop
Prepare Gaussian Input Files
You need two Gaussian input files (.gjf):
a) Large cluster (for matrix extraction)
A cluster with many unit cells (e.g., 11) so that the interior cells are bulk-like. This is used only to extract initial Fock/overlap matrices and coupling parameters. Hydrogen caps at the edges are fine – they will be discarded during extraction.
Example: CNT33_11_minbasis.gjf – an 11-cell (3,3) armchair CNT with STO-3G.
b) Device cluster (for SCF)
A smaller cluster matching the device region you will simulate. This file defines the geometry that Gaussian uses during the SCF loop. It should:
Contain exactly the atoms in your device (contact cells + interior cells)
Use the same basis set as the large cluster
Have NO hydrogen caps (the semi-infinite leads replace them)
Have coordinates taken from the interior of the large cluster
Example: CNTPeriodic5.gjf – 5 cells extracted from the interior of the
11-cell system, STO-3G, no caps.
Understand the Unit Cell Structure
For a periodic 1D system, identify:
Atoms per unit cell (
CPerLayer): e.g., 12 for a (3,3) armchair CNTOrbitals per atom (
Corbs): e.g., 5 for carbon with STO-3G (1s, 2s, 2px, 2py, 2pz)Orbitals per unit cell (
orbsPerCell = Corbs * CPerLayer): e.g., 60
The device is partitioned as:
| Left | Device | Right |
| Contact | Coupling ... Coupling | Contact |
| 1 cell | (N-2) cells | 1 cell |
The contact cells define the repeating unit of the semi-infinite leads. Buffer cells screen the contact self-energies from the active region.
Extract Matrices from the Large Cluster
import numpy as np
from scipy.linalg import fractional_matrix_power
from gauopen import QCBinAr as qcb
har_to_eV = 27.211386
Corbs = 5
CPerLayer = 12
orbsPerCell = Corbs * CPerLayer # 60
# Run Gaussian on the large cluster
bar = qcb.BinAr(debug=False, lenint=8, inputfile="CNT33_11_minbasis.gjf")
bar.update(model='b3lyp', basis='STO-3G', toutput='out.log',
chkname="CNT33_11_minbasis.chk", dofock='scf')
S_full = np.array(bar.matlist['OVERLAP'].expand())
P_full = np.array(bar.matlist['ALPHA SCF DENSITY MATRIX'].expand())
F_full = np.array(bar.matlist['ALPHA FOCK MATRIX'].expand()) * har_to_eV
a) Extract the device block
Choose the most central cells. For a 10-cell system extracting 5 cells:
nCells = 5
startCell = 3 # cells 3,4,5,6,7
ind1 = startCell * orbsPerCell # 180
ind2 = (startCell + nCells) * orbsPerCell # 480
F = F_full[ind1:ind2, ind1:ind2]
S = S_full[ind1:ind2, ind1:ind2]
b) Count electrons
This step is only needed if you plan to pass explicit alphas to
gauNEGF.scfE.NEGFE.setContact1D() (not used by Patterns A or C
below). neList/muList take effect only together with explicit
alphas – they are silently ignored on the contactList-only
auto-extraction path. When you do need it, use the physical electron
count of the unit cell – it is exact and basis-set independent:
# CNT example: 12 carbons per layer x 6 electrons per neutral C
ne_per_unit_cell = 12 * 6 # = 72
For a generic system the count is sum(Z_atom) over the atoms of one
unit cell, minus core electrons for ECP basis sets (e.g., 11 valence
electrons per Au atom in LANL2DZ).
Note
Earlier revisions of this guide computed ne via
np.trace(P @ S) on a sub-block of the device. That form
silently drops the non-orthogonal cross-term contribution
(\(\delta N\)) and gives a Mulliken count off by the
cross-term weight on systems where the contact overlap
stau is non-zero. Use the physical electron count instead;
it does not depend on the basis set or on whether the contact
is orthogonal.
c) Extract coupling matrices from the deep interior
Use the two most central cells of the large cluster for the most bulk-like coupling. These define the semi-infinite 1D chain:
# Interior cells 4 and 5 (deep inside the 10-cell cluster)
ic1 = np.arange(4 * orbsPerCell, 5 * orbsPerCell)
ic2 = np.arange(5 * orbsPerCell, 6 * orbsPerCell)
tau_F = F_full[np.ix_(ic1, ic2)] # inter-cell coupling (Fock)
tau_S = S_full[np.ix_(ic1, ic2)] # inter-cell coupling (overlap)
alpha_F = F_full[np.ix_(ic2, ic2)] # on-site energy (Fock)
alpha_S = S_full[np.ix_(ic2, ic2)] # on-site overlap
Key definitions:
Matrix |
Meaning |
Size |
|---|---|---|
|
On-site Fock for one bulk unit cell |
orbsPerCell x orbsPerCell |
|
On-site overlap for one bulk unit cell |
orbsPerCell x orbsPerCell |
|
Coupling Fock from cell n to cell n+1 |
orbsPerCell x orbsPerCell |
|
Coupling overlap from cell n to cell n+1 |
orbsPerCell x orbsPerCell |
|
Hopping between bulk cells (= tau for periodic) |
orbsPerCell x orbsPerCell |
For a periodic system, beta = tau because every inter-cell coupling is
identical.
Set Up NEGFE
from gauNEGF.scfE import NEGFE
# NEGFE inherits NEGF.__init__, so standard NEGF arguments apply
negf = NEGFE(fn='CNTPeriodic5', func='b3lyp', basis='STO-3G', fullSCF=False)
fn: stem of the device.gjffile (no extension)fullSCF=False: use Harris guess for initial DFT (faster)
To start from the Fock matrix extracted from the large cluster, call
negf.setFock(F) (F in eV) after the contacts and the voltage are
set. gauNEGF.scfE.NEGFE.setFock() rebuilds the contacts from the new
Fock with g.setF(F, mu1, mu2), computes the density that Fock implies
(including the Fermi search, if it is on) and hands that density to
Gaussian. It raises RuntimeError if setVoltage has not run yet. The
order is therefore:
negf.setContact1D(...) # contacts first (setVoltage needs them)
negf.setVoltage(0.0) # defines mu1/mu2
negf.setFock(F) # rebuild contacts + density from F
negf.SCF(...)
Three Usage Patterns for setContact1D
The setContact1D method supports three different calling patterns, depending
on how much information you have about the bulk electronic structure. Choose the
pattern that matches your workflow.
Pattern A: Auto-Extract from Device Fock (recommended)
Use this pattern when you have a device cluster extracted from a larger
DFT calculation. setContact1D reads the contact-cell and inter-cell
coupling matrices directly from the device Fock and overlap. No explicit
matrices required.
negf = NEGFE(fn='SiNanowire', func='b3lyp', basis='STO-3G')
inds = negf.setContact1D(
[[1, 2, 3], [4, 5, 6]], # contactList: atoms per contact
symmetrize_contacts=True, # True if both contacts same material
eta=1e-3 # broadening (eV)
)
negf.setVoltage(0.0)
negf.setFock(F) # optional: start from the extracted Fock (eV)
negf.SCF(1e-2, 0.02, 100)
Key arguments:
contactList: 1-indexed atom numbers within the device
.gjffile. Left contact is first unit cell; right contact is last unit cell.symmetrize_contacts: Set
Truewhen both contacts are the same material (e.g., periodic nanotube, nanowire). This averages the on-site Fock blocks during SCF to prevent artificial symmetry breaking from opposite directional coupling signs in sigma_L vs sigma_R. Pattern A auto-detects this asTrueby default (no explicittauListis given here), so passing it explicitly is for clarity, not required.eta: Broadening (eV). Larger values (1e-3) speed up surface Green’s function convergence but smear sharp features. See Configuration and Tuning for ETA tuning.
For the full signature including tauList, stauList, alphas,
aOverlaps, betas, bOverlaps, and neList (used when overriding
auto-extraction), see gauNEGF.scfE.NEGFE.setContact1D() or Pattern B
below.
Pattern B: Custom Coupling with Auto Onsite (tauList only)
Use this pattern when you have only the inter-cell coupling matrices and want the on-site block computed automatically from the device Fock matrix. This is useful when the device onsite approximation is adequate.
# Custom tau, auto alpha/beta
negf = NEGFE(fn='CNT_device', func='b3lyp', basis='STO-3G')
# Only provide coupling; on-site blocks are extracted from the device Fock
inds = negf.setContact1D(
[[1, 2, 3, 4, 5, 6], [7, 8, 9, 10, 11, 12]], # contact atom indices
tauList=[custom_tau_left, custom_tau_right], # explicit inter-cell coupling
eta=1e-3
)
negf.setVoltage(0.0, fermiMethod='poly')
negf.setFock(F) # optional: F is the device Fock in eV (all device atoms)
negf.SCF(1e-3, 0.02, 1000)
Note
This pattern is useful when your bulk structure matches the device
geometry well enough that extracting alpha and beta from the device Fock
is acceptable. If this assumption is invalid (e.g., strongly distorted
contacts), pass the explicit matrices listed in
gauNEGF.scfE.NEGFE.setContact1D().
Pattern C: Auto-Extraction from DFT (contactList only)
Use this pattern when you do NOT have pre-extracted bulk matrices and want
gauNEGF to extract contact parameters directly from Gaussian. The coupling
matrices are auto-computed from the DFT geometry stored in the device .gjf.
# minimal specification
negf = NEGFE(fn='C2', func='b3lyp', basis='lanl2dz', fullSCF=False)
# Only atoms; matrices auto-extracted from Gaussian
inds = negf.setContact1D(
[[1], [2]], # contactList: atoms per contact
eta=1e-4
)
# Let Fermi search run -- do NOT pass an explicit fermi energy here
negf.setVoltage(0.0, fermiMethod='poly')
negf.SCF(1e-2, 0.02, 100)
negf.SCF(1e-4, 0.02, 1000, pulay=False)
Warning
Do not pass neList with auto-extraction (Pattern C).
neList/muList only take effect together with explicit
alphas (see gauNEGF.scfE.NEGFE.setContact1D()). On this
contactList-only path, any neList you pass is silently ignored –
it is not an error, it just has no effect.
The Fermi search target is set by the fermiCount argument of
setContact1D. The default, fermiCount='contact', picks the Fermi
energy that makes the contact atoms (left and right together) neutral:
their Mulliken population plus the device-contact cross term must equal
nelecContacts, the sum of the contact atoms’ nuclear charges as
Gaussian reports them (bar.atmchg). When the contacts cover the
whole device (a two-cell device) this is the whole-device count
bar.ne. fermiCount='device' counts the whole device plus the
cross term; fermiCount='device_bare' counts the whole device without
it. fermiMethod='predict' cannot be combined with
fermiCount='contact' (it raises ValueError).
Warning
With Pattern C (no explicit tau/alpha/beta), do not pass an explicit
Fermi energy as the second positional argument to setVoltage – the
electron count becomes unphysical. Let Fermi search run via the default
fermiMethod='muller' or pick another method explicitly:
negf.setVoltage(0.0) # uses default 'muller'
# or
negf.setVoltage(0.0, fermiMethod='poly') # explicit method
See Configuration and Tuning for the method comparison and selection guidance.
Run the SCF
# Let the Fermi search run (default fermiMethod='muller')
negf.setVoltage(0.0)
# Run SCF: tolerance, damping, max iterations
negf.SCF(1e-3, 0.02, 1000)
# Save results
negf.saveMAT('output.mat')
Note
negf.g.fermiList exists only on the explicit-alphas branch of
setContact1D (populated inside setF). Patterns A/B/C above all
leave alphas=None, so negf.g.fermiList is not available and
negf.g.fermiList[0] would raise AttributeError – and even where
it exists, passing it as an explicit fermi here would re-pin the fermi
the same way the Pattern C warning above cautions against. Use
negf.setVoltage(0.0) (as shown) to let the search run.
SCF parameters
tolerance (1e-3): convergence criterion on the density matrix change
damping (0.02): linear mixing parameter – smaller is more stable but slower. Range: 0.02 to 0.05 for routine use; wider excursions (down toward 0.005 or up to 0.1) are for diagnosing a specific problem – see Configuration and Tuning.
max iterations (1000): safety cap
Post-SCF: Transmission and Current
from gauNEGF.transport import cohTransE
from scipy import io
Elist = np.linspace(-5, 5, 500)
T = cohTransE(Elist + negf.fermi, negf.F * har_to_eV, negf.S, negf.g)
io.savemat('transmission.mat', {'Elist': Elist, 'fermi': negf.fermi, 'T': T})
For IV curve sweeps or multi-temperature workflows, see Guide 5: Workflow Recipes.
Common Pitfalls
Too few cells: Using only 3 cells (1+1+1) gives no buffer between contacts; the self-energies overlap on the single device cell. Add interior cells until the on-site Fock for the central cell looks bulk-like.
Edge-contaminated coupling: Extracting tau/alpha from the edge of the large cluster gives non-bulk-like values. Always extract from the deep interior.
Mismatched geometries: The device
.gjfmust have coordinates taken from the same large cluster used for matrix extraction. Do not mix independently generated geometries.Wrong basis in device .gjf: The NEGFE constructor overrides the basis via the
basisargument, but using a checkpoint from a different basis can cause issues. Delete stale.chkfiles when changing basis sets.Forgetting .conj().T for right contact: In a periodic system, the right contact coupling is the Hermitian conjugate of the left. Passing the same matrix for both creates an asymmetric system.
H caps in device .gjf: The device file should have bare dangling bonds at the edges. The semi-infinite leads replace the caps. Including H atoms adds spurious states.
Passing an explicit Fermi energy with Pattern C: With auto-extraction (Pattern C), passing the second positional argument to
setVoltagedisables the Fermi search and gives an unphysical contact electron count. Either omit it (defaultfermiMethod='muller'runs) or pass only thefermiMethod=keyword.Pattern B defaults to symmetrize_contacts=False: the flag auto-detects
Trueonly whentauListis not passed (Patterns A and C get it automatically). If you pass an explicittauList(Pattern B) for two same-material leads, setsymmetrize_contacts=Trueexplicitly – otherwise the on-site Fock blocks are not averaged and parity symmetry can break during SCF.