Chapter 5
12 min read
Section 42 of 70

Density of States

Electronic Structure and Properties

Learning Objectives

By the end of this section you should be able to:

  1. Define the density of states g(E)g(E) in plain language — "how many electronic states are available at energy E" — and write it as a delta-function sum over the band structure of Section 5.1.
  2. Explain the inverse-velocity rule: g(E)1/kEng(E) \propto 1/|\nabla_{\mathbf{k}} E_n|. See instantly why flat bands give big DOS and steep bands give small DOS.
  3. Recognise the three universal free-electron shapes and remember which one belongs to which dimension: 1/E1/\sqrt{E} (1-D), constant (2-D), and E\sqrt{E} (3-D).
  4. Identify van Hove singularities on a DOS plot and connect them to critical points of En(k)E_n(\mathbf{k}) — band edges, saddle points, ridges.
  5. Read a projected DOS and answer in one sentence: "the VBM is mostly Se 4p, the CBM is mostly Cd 5s."
  6. Run a VASP DOS calculation: pick the right ISMEAR (tetrahedron 5-5 for static DOS, Gaussian 00 for semiconductors during SCF, Methfessel–Paxton 11 for metals), set NEDOS, write LORBIT=11 for projections, and plot the result with five lines of pymatgen.
One-line preview: the band structure tells you where in reciprocal space the energies live; the density of states tells you how many of them sit at each energy. Almost every experimental probe — heat capacity, photoemission, optical absorption, Pauli susceptibility — measures an integral over g(E)g(E), not En(k)E_n(\mathbf{k}) directly. So this is the form of electronic structure that the laboratory actually sees.

The Question DOS Answers

Last section we built the band structure En(k)E_n(\mathbf{k}) — a beautiful object, but a difficult one. It is a stack of 3-D surfaces in a 3-D Brillouin zone, and no experiment ever sees it directly. Photoemission gets you a smeared cross-section. Heat capacity gets you one number. Optical absorption gets you a 1-D spectrum. What every one of these probes secretly cares about is the same simpler question:

The DOS question

If I scan the energy axis from -\infty to ++\infty, how many electronic states will I find in a window of width dEdE around each energy?

That number — divided by the volume of the crystal and by dEdE — is the density of states g(E)g(E). It is one number per energy. Plot it on a sheet of paper and you have what every DFT post-processing tool spits out as DOSCAR: a 1-D function that contains an enormous amount of information about the crystal's electrons.

Why one number per energy is enough for so many properties is the magic. Two examples to anchor your intuition:

  • Specific heat at low temperature in a metal is CV=π23kB2Tg(EF)C_V = \tfrac{\pi^2}{3}\,k_B^2\,T\,g(E_F). The whole answer is set by one number: g(EF)g(E_F), the DOS evaluated right at the Fermi level.
  • Optical absorption at photon energy ω\hbar\omega is, in the simplest dipole approximation, an integral gv(E)gc(E+ω)M2dE\int g_v(E)\,g_c(E + \hbar\omega)\,|M|^2\,dE — the joint density of states, weighted by a matrix element. The shape of an absorption spectrum is, to leading order, a convolution of two DOS curves.

Definition — Counting States Per Energy

Formally, the density of states (per unit volume, per spin) is

g(E)  =  1VnBZδ ⁣(EEn(k))d3k(2π)3.g(E) \;=\; \frac{1}{V}\sum_{n}\int_{\text{BZ}}\,\delta\!\big(E - E_n(\mathbf{k})\big)\,\frac{d^3k}{(2\pi)^3}.

Read it slowly. The Dirac delta δ(EEn(k))\delta(E - E_n(\mathbf{k})) is an infinitely-sharp filter that fires when the band energy En(k)E_n(\mathbf{k}) hits the energy EE we are asking about. Integrating over the Brillouin zone counts every wavevector at which a state with that energy exists. Summing over band index nn bundles together all bands. Dividing by the crystal volume VV turns the count into a density.

An equivalent and more useful form

Using the rule δ(f(k))=iδ(kki)/fki\delta(f(\mathbf{k})) = \sum_i \delta(\mathbf{k} - \mathbf{k}_i) / |\nabla f|_{\mathbf{k}_i} (where ki\mathbf{k}_i are the roots of ff), we can rewrite the DOS as a surface integral over the constant-energy surface Sn(E)S_n(E) defined by En(k)=EE_n(\mathbf{k}) = E:

g(E)  =  nSn(E)dS(2π)3kEn(k).g(E) \;=\; \sum_n \int_{S_n(E)} \frac{dS}{(2\pi)^3 \,|\nabla_{\mathbf{k}} E_n(\mathbf{k})|}.

This is the form to remember. The DOS at energy EE is the area of the constant-energy surface divided by the gradient of the band — the bigger the surface and the slower the band climbs through it, the more states pile up there.


Interactive — From E(k) to g(E)

The cleanest way to internalise the formula is to see it executed on a one-dimensional band. Below, the left panel is the familiar tight-binding cosine E(k)=2tcoskE(k) = -2t\cos k. The right panel is the same data, binned into 90 small energy windows: a histogram of E(ki)E(k_i) over 2400 evenly-spaced k-points. Drag tt to see the band stretch and the histogram scale with it. Toggle "add a second band" to watch new peaks appear when a gap opens.

Loading band → DOS visualizer…

Three observations that are not obvious until you have played for a minute:

  1. The peaks of the histogram sit exactly at the band edges. At k=0k = 0 and k=±πk = \pm \pi the slope of the band vanishes. The energy spends "extra time" near those values, so many k-points end up in those bins.
  2. The middle of the band is shallow. Near k=±π/2k = \pm \pi/2 the band is at its steepest (group velocity v=2tsinkv = 2t \sin k is maximum). The histogram dips because the band rushes through those energies and relatively few k-points stop there.
  3. The band width 4t4t scales the whole picture. Doubling tt doubles the band width and halves the peak DOS — the area under g(E)g(E) is conserved (one state per atom always).

The Inverse-Velocity Rule

The histogram you just watched is a numerical approximation of the formula

g(E)dE  =  12πk:E(k)=EdEdE/dk.g(E)\,dE \;=\; \frac{1}{2\pi}\,\sum_{k\,:\,E(k)=E} \frac{dE}{|dE/dk|}.

In words: the density of states at energy EE is the inverse of how fast the band moves through EE. Mathematically the same thing as the constant-energy-surface formula above, but in 1-D it collapses to something you can reason about with one finger:

Region of band|dE/dk|g(E)Visual
Steep slope (middle of band)largesmallvalley in DOS
Shallow slope (extremum)→ 0→ ∞spike in DOS (van Hove)
Flat band≈ 0 everywherehugenarrow tall peak
Two bands cross with same slopetwo contributions adddoublesstep in DOS

The pencil test

Print any band structure plot and put your pencil flat against a band. Drag the pencil up the energy axis. Wherever the pencil lies tangent to the band — where it touches the band over a long horizontal stretch — you will find a DOS peak. Wherever the band crosses the pencil at a steep angle, the DOS at that energy is small. That is the entire intuition of g(E)g(E) in one gesture.


Free Electrons in 1, 2, and 3 Dimensions

Before crystals, before bands, the simplest model is the free-electron gas: E=2k2/2mE = \hbar^2 k^2 / 2m, a parabola. Even for this baby model the DOS depends dramatically on dimensionality because the constant-energy surface E(k)=EE(\mathbf{k}) = E has different geometry in different dimensions.

1-D wire

In 1-D the constant-energy "surface" is just two points, k=±2mE/k = \pm \sqrt{2mE}/\hbar. Plug into the general formula:

g1D(E)  =  1π2m21E.g_{1D}(E) \;=\; \frac{1}{\pi}\sqrt{\frac{2m}{\hbar^2}}\,\frac{1}{\sqrt{E}}.

States pile up at the band bottom — a true 1/E1/\sqrt{E} divergence. This is the signature of single-walled carbon nanotubes and quantum wires.

2-D sheet

In 2-D the constant-energy surface is a circle of radius k=2mE/k = \sqrt{2mE}/\hbar with circumference 2πk2\pi k. The gradient E=2k/m|\nabla E| = \hbar^2 k/m. The two cancel:

g2D(E)  =  mπ2(constant for E>0).g_{2D}(E) \;=\; \frac{m}{\pi \hbar^2} \quad (\text{constant for } E > 0).

A perfect step function at E=0E = 0. Each free-electron subband in a 2-D quantum well adds another step — the famous staircase DOS seen in semiconductor heterostructures.

3-D bulk

In 3-D the constant-energy surface is a sphere of radius kk with surface area 4πk24\pi k^2. After dividing by (2π)3E(2\pi)^3 |\nabla E|:

g3D(E)  =  12π2(2m2)3/2E.g_{3D}(E) \;=\; \frac{1}{2\pi^2}\Big(\tfrac{2m}{\hbar^2}\Big)^{3/2}\,\sqrt{E}.

Starts at zero and grows as E\sqrt{E}. This is the curve every solid-state textbook prints when introducing free electrons in a metal — and the leading edge of every parabolic 3-D band, which is why it dominates the bottom of the conduction band in most semiconductors.


Interactive — DOS by Dimensionality

Click between the three dimensions to see the shapes side by side. Drop a Fermi level on the plot to read off how many states sit below it — the electron filling is the area under g(E)g(E) up to EFE_F.

Loading dimensional DOS comparison…

Why this matters for real materials

The dimensionality on display is not academic. A 2-D electron gas in a quantum-well laser, a graphene sheet (which adds a Dirac cone to the step), and a quantum-wire field-effect transistor all inherit one of these shapes near their band edges. Quantum confinement changes the dimensionality the electrons see — and therefore the DOS shape — which is the entire reason a semiconductor laser's threshold current drops by orders of magnitude when you go from a 3-D bulk gain region to a 1-D quantum wire. We will return to this in Section 5.9 when we shrink CdSe down to a quantum dot.


Van Hove Singularities

Real bands are not parabolas. They have multiple maxima, minima, and saddle points — places where kEn=0\nabla_{\mathbf{k}} E_n = 0. Léon van Hove showed in 1953 that every such critical point produces a non-analytic feature in g(E)g(E). The shape of the feature depends on dimension and on the nature of the critical point:

DimCritical pointLocal E(k)DOS feature
1-Dminimum / maximumE ≈ E₀ + α(k − k₀)²1/√(E − E₀) divergence
2-Dminimum / maximumE ≈ E₀ + α k²step (constant g for E > E₀)
2-DsaddleE ≈ E₀ + α(kₓ² − k_y²)logarithmic divergence ln|E − E₀|
3-Dminimum / maximumE ≈ E₀ + α k²kink: g ∝ √(E − E₀)
3-DsaddleE ≈ E₀ + α k_∥² − β k_⊥²kink with finite jump in slope

The 2-D logarithmic singularity is the famous van Hove peak in graphene: the saddle point at the M-point of the hexagonal Brillouin zone gives a logarithmic divergence in the DOS at ±t\pm t. Every twisted-bilayer-graphene paper that talks about "flat-band physics" near a magic angle is secretly talking about the same physics: pushing van Hove peaks down to the Fermi level, where the divergence triggers superconductivity or magnetism through diverging response functions.

Reading singularities, not just locations

A practical materials scientist learns to read DOS plots not just for the gap but for the character of the features: a sharp narrow peak suggests a saddle point or a flat band; a square-root edge suggests a 3-D parabolic minimum; a true step is a 2-D parabolic minimum. These shapes are diagnostic of dimensionality and topology.


Reading a DOS Plot

A standard DOS plot is a 1-D graph: energy on the x-axis (almost always with EFE_F shifted to zero), DOS on the y-axis. Sometimes the plot is rotated 90° so the energy axis is vertical and aligned with a band-structure plot beside it — that combined plot is the single most informative figure a DFT calculation produces.

Visual featureWhat it means
Wide gap (zero DOS region) at E = 0Insulator or semiconductor
Finite DOS at E = 0Metal — and g(E_F) sets the heat capacity
Sharp peakFlat band or saddle point — likely correlated physics
√E onset above gapBottom of a 3-D parabolic conduction band
StepBottom of a 2-D subband (quantum well) or flat plateau
Spin-up ≠ spin-downMagnetic ground state (will see in §5.6)
Mid-gap peakDefect or impurity state (CdSe with Mn — see Ch. 7)

Every one of those readings is a habit worth building. A practiced eye can spot the band gap, judge the metal-vs-semiconductor question, and guess the orbital character of the band edges in less than ten seconds from a DOS plot alone — without ever touching the band-structure plot.


Projected DOS — Asking Which Atom and Which Orbital

The total DOS counts all states. But the most useful question is usually finer: which atoms contribute at the Fermi level? which orbitals make up the valence band maximum? The answer is the projected DOS:

gα,(E)  =  n,kϕα,ψn,k2δ ⁣(EEn(k))g_{\alpha,\ell}(E) \;=\; \sum_{n,\mathbf{k}}\,\big|\langle \phi_{\alpha,\ell}\,|\,\psi_{n,\mathbf{k}}\rangle\big|^2\,\delta\!\big(E - E_n(\mathbf{k})\big)

where ϕα,\phi_{\alpha,\ell} is an atomic-like orbital localised on atom α\alpha with angular momentum \ell. In words: project every Bloch state onto a chosen atomic orbital, square the amplitude, and bin the result into the DOS sum.

For our flagship system CdSe, the standard story readable in a single PDOS plot is:

  • Valence band maximumSe 4p — the anion gives the holes.
  • Conduction band minimumCd 5s — the cation accepts the electrons.
  • Cd 4d sits as a narrow band ~7 eV below VBM (a sharp PDOS peak — Cd's closed semicore d-shell).
  • Mn 3d in doped Mn:CdSe (Chapter 7) appears as mid-gap impurity peaks split by the crystal field — the entire reason Mn:CdSe has tunable luminescence.

The orbital fingerprint

A PDOS is the DFT version of an X-ray photoelectron spectroscopy (XPS) experiment. Each elemental peak in XPS corresponds to a sharp PDOS feature dominated by core-like or semi-core orbitals. If your calculated PDOS doesn't reproduce the experimental XPS peak positions — your pseudopotential, your exchange-correlation functional, or both, are suspect.


Computing DOS in VASP — Tetrahedron and Smearing

A DOS calculation in VASP is the same two-step pattern as a band structure: a converged SCF run, followed by a non-SCF run with a denser k-grid. The differences are only in how you tell VASP to integrate the delta function — that is the role of ISMEAR.

ISMEARMethodUse when
0Gaussian smearing of width SIGMASemiconductors and insulators during SCF; safe everywhere
1, 2Methfessel–Paxton order 1 or 2Metals during SCF — preserves total energy ordering
−5Tetrahedron with Blöchl correctionsStatic DOS / band-structure runs (NOT for forces or relaxation)
−4Tetrahedron without correctionsSame — works also for k-grids without symmetry

The cardinal smearing rule

Use ISMEAR=0\texttt{ISMEAR}=0 with SIGMA=0.05\texttt{SIGMA}=0.05 eV during the SCF and relaxation runs of a semiconductor. Switch to ISMEAR=5\texttt{ISMEAR}=-5 only for the static DOS / band-structure run on top of a converged charge density. Tetrahedron gives sharp, accurate DOS curves but is incompatible with k-meshes that don't fully sample symmetry-equivalent points and produces wrong forces — never use it for relaxations.

Step 1 — converged SCF

📝text
1# INCAR — SCF for CdSe
2SYSTEM = CdSe SCF for DOS
3PREC   = Accurate
4ENCUT  = 400          # converged cutoff (eV)
5EDIFF  = 1E-6
6ISMEAR = 0            # Gaussian during SCF — safe for semiconductors
7SIGMA  = 0.05
8LCHARG = .TRUE.       # write CHGCAR for step 2
9LWAVE  = .FALSE.
📝text
1# KPOINTS — uniform grid for SCF
2Monkhorst-Pack
30
4Gamma
58 8 8
60 0 0

Step 2 — non-SCF DOS run (denser k-grid + tetrahedron)

📝text
1# INCAR — DOS run
2SYSTEM   = CdSe DOS
3PREC     = Accurate
4ENCUT    = 400
5ICHARG   = 11         # read CHGCAR, do not update density
6ISMEAR   = -5         # tetrahedron with Blöchl corrections
7NEDOS    = 3001       # number of energy points in DOSCAR
8EMIN     = -15        # lowest energy in DOS window (eV)
9EMAX     = 15         # highest energy in DOS window (eV)
10LORBIT   = 11         # write per-atom, per-orbital projections
11NBANDS   = 32
📝text
1# KPOINTS — denser uniform grid
2Monkhorst-Pack
30
4Gamma
516 16 16
60 0 0

What each tag actually does

  • ICHARG=11 — read the SCF charge density from CHGCAR and freeze it; only diagonalise the Hamiltonian on the new k-grid. Saves time and guarantees the SCF and DOS use the same self-consistent potential.
  • NEDOS=3001 — number of points sampled along the energy axis when writing DOSCAR. Higher resolution means smoother curves; the cost is purely disk and parsing time.
  • EMIN/EMAX — energy window written to DOSCAR. Make it wide enough to include the deep semicore peaks (Cd 4d at −7 eV) you want to see.
  • LORBIT=11 — turn on the projection of each Bloch state onto atomic orbitals using the PAW partial-wave scheme. Without this you get the total DOS only — no PDOS.
  • 16 × 16 × 16 k-grid — roughly four times denser than the SCF grid. Tetrahedron integration converges quickly with denser grids; the cost is non-SCF and so cheap.

Output files

FileContainsHow you read it
DOSCARTotal DOS + PDOS (if LORBIT=11), one block per atompymatgen / vaspkit / sumo
vasprun.xmlSame data + Fermi level + INCAR + structure metadatapymatgen Vasprun(parse_dos=True)
PROCARPer-band per-orbital projections (band-structure-style)pymatgen / vasppy / sumo
OUTCARRun log; grep for E-fermi to verify Fermi levelshell, awk

Plotting DOS with pymatgen

With those files in hand, generating a publication-quality DOS plot — total + element-projected + orbital-projected — takes about thirty lines of pymatgen. Click any line below to see what every variable, function argument, and library call is doing in plain language.

Plotting CdSe DOS — line-by-line trace
🐍plot_dos.py
1from pymatgen.io.vasp.outputs import Vasprun

pymatgen is the Python Materials Genomics library — the de-facto standard tool for parsing VASP output files. Vasprun is the class that reads vasprun.xml, the XML file VASP writes at the end of every run. It contains everything: eigenvalues, k-points, the DOS, the Fermi level, the INCAR, and the structure.

EXECUTION STATE
📚 pymatgen.io.vasp.outputs = Submodule containing parsers for VASP output files (OUTCAR, vasprun.xml, EIGENVAL, DOSCAR, CHGCAR, etc.). All return rich Python objects, not raw text.
Vasprun = The XML parser class. Reads the entire run into memory; you then ask it for whichever piece you want via attributes: vr.complete_dos, vr.eigenvalues, vr.final_energy, vr.efermi, vr.parameters (the INCAR), vr.kpoints, etc.
2from pymatgen.electronic_structure.core import OrbitalType

OrbitalType is an enum used to filter projected-DOS data by angular momentum: OrbitalType.s, OrbitalType.p, OrbitalType.d, OrbitalType.f. Using the enum is safer than passing strings — typos are caught at import time.

EXECUTION STATE
OrbitalType = Enum with members s, p, d, f. Example use: cd_spd[OrbitalType.s] returns the s-projected DOS for Cd, summed over all atomic Cd sites.
3from pymatgen.core.periodic_table import Element

Element wraps element symbols ("Cd", "Se", …) into a typed Python object. Used here to specify which element we want a projected DOS for.

EXECUTION STATE
Element = Class wrapping a symbol. Element("Cd") gives an object with .symbol = 'Cd', .Z = 48, .X (electronegativity), etc. Required by get_element_spd_dos().
4from pymatgen.electronic_structure.plotter import DosPlotter

DosPlotter is a small class that aggregates several DOS curves (total, per element, per orbital) into one matplotlib figure with consistent styling.

EXECUTION STATE
DosPlotter = Helper class. Methods: add_dos(label, dos_object) to register a curve; get_plot(...) to render the figure. Can stack curves vertically or overlay them.
5import matplotlib.pyplot as plt

matplotlib is Python's standard plotting library. We use it here only for a couple of post-processing tweaks (vertical line at E_F, savefig) — DosPlotter does the heavy lifting.

EXECUTION STATE
matplotlib.pyplot = Procedural plotting interface modeled after MATLAB. plt.axvline draws a vertical line; plt.savefig writes the current figure to disk.
7vr = Vasprun("vasprun.xml", parse_dos=True)

Open and parse the VASP output XML. parse_dos=True tells pymatgen to deserialize the <dos> block — without this flag, the DOS is skipped to save time.

EXECUTION STATE
📚 Vasprun(filename, parse_dos, parse_eigen, ...) = Constructor reads the XML. Lazy parsers are gated by keyword arguments because vasprun.xml can be hundreds of megabytes for big runs.
⬇ arg 1: "vasprun.xml" = Path to the VASP output file. By convention written to the run directory at the end of every calculation.
⬇ arg 2: parse_dos=True = Tells the parser to populate vr.complete_dos. Without it, accessing complete_dos returns None.
⬆ result: vr = Vasprun object. Useful attributes: vr.complete_dos, vr.efermi, vr.eigenvalues, vr.final_energy, vr.parameters, vr.structures.
9dos = vr.complete_dos

Pull the CompleteDos object out of the parsed run. CompleteDos holds the total DOS plus every site- and orbital-projected DOS — everything you need to break the spectrum down by atom and angular momentum.

EXECUTION STATE
vr.complete_dos = Type: CompleteDos. Internally a dict mapping (site, orbital) → Dos curve, plus the total DOS. Energy grid is shared across all curves; each curve is a NumPy array of densities at those energies.
→ why 'complete'? = Distinguishes from the bare Dos class (just total) and Spin.up/Spin.down accessors. CompleteDos is the union of all projected pieces.
10e_fermi = dos.efermi

Read the Fermi level from the DOS object. VASP writes this to the OUTCAR (search for E-fermi) and pymatgen extracts it. For semiconductors it sits inside the gap by convention; for metals it sits at the top of the occupied states.

EXECUTION STATE
dos.efermi = Float, units of eV. For our converged Mn:CdSe run, expect a value around 3–5 eV (absolute scale). DosPlotter will subtract this so plots show E − E_F.
11print(f"Fermi level: {e_fermi:.3f} eV")

Sanity-check print. Always verify the Fermi level matches what OUTCAR reports — if it doesn't, the parse went sideways or you opened the wrong file.

EXECUTION STATE
f-string = Python's formatted string literal. {e_fermi:.3f} substitutes the value of e_fermi formatted to 3 decimal places.
→ typical output = Fermi level: 3.412 eV
13plotter = DosPlotter(zero_at_efermi=True, stack=False, sigma=0.05)

Create a fresh plotter object. We will add several DOS curves to it before rendering.

EXECUTION STATE
📚 DosPlotter(zero_at_efermi, stack, sigma) = Constructor configures how all curves added later will be displayed.
⬇ zero_at_efermi=True = Subtract E_F from every energy on the x-axis. After this, E = 0 means the Fermi level — the universal convention for DOS plots. With False, energies are absolute (in the eigenvalue zero of the pseudopotential).
⬇ stack=False = Overlay the curves rather than stacking them additively. Stack=True is useful for showing 'PDOS adds up to total DOS' visually, but overlay is cleaner for reading.
⬇ sigma=0.05 = Gaussian smearing width applied to the raw DOS for plotting only (eV). Smooths out tetrahedron-method 'bumpiness' so the curve is readable. Has nothing to do with the SCF SIGMA in INCAR — purely cosmetic.
⬆ result: plotter = Empty DosPlotter. Internal dict of {label: dos_object}. Filled in the next few lines via add_dos().
15plotter.add_dos("Total DOS", dos)

Register the total DOS curve under the label 'Total DOS'. The label becomes the legend entry.

EXECUTION STATE
📚 plotter.add_dos(label, dos) = Adds one curve to the figure. Method: simply stores the (label, dos) pair in a dict — nothing is plotted until get_plot() is called.
⬇ arg 1: "Total DOS" = String shown in the legend. Use a short label that distinguishes this curve from the others (Total vs Cd vs Se vs orbital projections).
⬇ arg 2: dos = The CompleteDos object. add_dos accepts both bare Dos and CompleteDos — for CompleteDos it pulls out the total automatically.
17element_dos = dos.get_element_dos()

Sum the orbital-projected DOS over each chemical element. For CdSe this returns two curves: one summed over all Cd sites and orbitals, one summed over all Se sites and orbitals. This is the 'who contributes here?' decomposition.

EXECUTION STATE
📚 dos.get_element_dos() = Method on CompleteDos. Aggregates site-projections by element so you see one curve per chemical species.
⬆ result: element_dos = Dict {Element('Cd'): Dos, Element('Se'): Dos}. Keys are Element objects; values are Dos (energies + densities arrays).
→ why elements? = Most practical questions are 'is the VBM mostly Se or mostly Cd?' Element-projected DOS answers that in one glance.
18for element, edos in element_dos.items():

Iterate over the (Element, Dos) pairs. Standard Python dict iteration. We will add each element's curve to the plotter with its symbol as the label.

LOOP TRACE · 2 iterations
iter 1: element=Element('Cd'), edos=<Dos with NEDOS energies>
element.symbol = 'Cd'
edos = Dos object: energies (length 3001), densities {Spin.up: array, Spin.down: array}
iter 2: element=Element('Se'), edos=<Dos with NEDOS energies>
element.symbol = 'Se'
edos = Dos object: energies (length 3001), densities {Spin.up: array, Spin.down: array}
19plotter.add_dos(f"{element.symbol} (all orbitals)", edos)

Register the element-projected DOS with a human-readable label. f-string interpolates the element symbol so the legend reads e.g. 'Cd (all orbitals)'.

EXECUTION STATE
label string = 'Cd (all orbitals)' on iteration 1, 'Se (all orbitals)' on iteration 2.
edos = Element-summed Dos. Adding it to the same plotter overlays it on the total DOS curve.
21cd_spd = dos.get_element_spd_dos(Element("Cd"))

Ask for the s/p/d-decomposed DOS for one element only. Returns a dict keyed by OrbitalType. This lets you see which angular momentum dominates a feature — e.g. the CBM in CdSe is mostly Cd 5s.

EXECUTION STATE
📚 dos.get_element_spd_dos(element) = Method on CompleteDos. Aggregates by (element, l) where l ∈ {s, p, d}. Different from get_element_dos(), which sums over l.
⬇ arg: Element("Cd") = Element object specifying which chemical species to project onto. Pass an Element, not a string.
⬆ result: cd_spd = Dict {OrbitalType.s: Dos, OrbitalType.p: Dos, OrbitalType.d: Dos}. Each Dos is the sum over all Cd atoms of the (s|p|d) channel.
22plotter.add_dos("Cd 5s", cd_spd[OrbitalType.s])

Pull the s-channel out of the dict and register it. Useful for tagging the conduction band: in II-VI semiconductors like CdSe, the CBM is dominated by the cation s state.

EXECUTION STATE
cd_spd[OrbitalType.s] = The s-projected Dos for Cd. By convention in CdSe (which has Cd in [Kr] 4d¹⁰ 5s²) the relevant s state is the 5s — hence the label.
→ expected feature = A peak just above E = 0 (the conduction band minimum). If your plot shows this, the band-edge character is confirmed.
24se_spd = dos.get_element_spd_dos(Element("Se"))

Same call as line 21, but for selenium. Builds a fresh dict of OrbitalType → Dos for Se.

EXECUTION STATE
se_spd = Dict {OrbitalType.s: Dos, OrbitalType.p: Dos, OrbitalType.d: Dos} for Se.
25plotter.add_dos("Se 4p", se_spd[OrbitalType.p])

Add the p-projected Se DOS. In CdSe the valence band maximum is dominated by Se 4p — this curve should peak just below E = 0.

EXECUTION STATE
se_spd[OrbitalType.p] = p-projected Se Dos. Anion 4p is the standard VBM character for II-VI semiconductors.
→ expected feature = A pronounced shoulder right below E = 0 (valence band maximum). Together with the Cd 5s peak above zero, this is the optical-transition story of CdSe in two curves.
27ax = plotter.get_plot(xlim=(-6, 6), ylim=(0, 18))

Render every registered curve into a matplotlib Axes object. xlim crops the energy range; ylim caps the DOS axis so the figure isn't dominated by deep core peaks or tall semicore bands.

EXECUTION STATE
📚 plotter.get_plot(xlim, ylim, ...) = Builds the matplotlib figure: applies sigma smearing, subtracts E_F (because zero_at_efermi=True), draws every registered curve in a distinct color with the given labels, and returns the Axes for further customization.
⬇ xlim=(-6, 6) = Energy window in eV around E_F. Six eV on each side is a sensible default for most semiconductors — captures the upper valence bands and the bottom of the conduction band without showing irrelevant deep states.
⬇ ylim=(0, 18) = DOS axis range (states/eV/cell). Pick a value that keeps the tallest peak inside the frame; eyeball-tune after a first run.
⬆ result: ax = matplotlib.axes.Axes — the active subplot. All subsequent matplotlib calls (axvline, set_title, etc.) operate on this axes.
28plt.axvline(0, color="black", linestyle="--", linewidth=0.6)

Draw a vertical dashed line at E = 0 — i.e. the Fermi level — so the eye instantly distinguishes filled from empty states. Cosmetic but nearly universal in published DOS plots.

EXECUTION STATE
📚 plt.axvline(x, color, linestyle, linewidth) = matplotlib helper that draws a vertical line spanning the full y-range of the current axes.
⬇ arg 1: x=0 = x-coordinate of the line in data units. Because zero_at_efermi=True earlier, x=0 is the Fermi level.
⬇ color="black" = Line color — black gives high contrast over colored DOS curves.
⬇ linestyle="--" = Dashed pattern. Solid would compete visually with the DOS curves.
⬇ linewidth=0.6 = Thin line — present but unobtrusive.
29plt.savefig("dos_cdse.png", dpi=200, bbox_inches="tight")

Write the figure to disk at publication resolution. Always pass bbox_inches='tight' so matplotlib doesn't crop the legend or labels.

EXECUTION STATE
📚 plt.savefig(fname, dpi, bbox_inches) = Persists the current figure. Format inferred from extension (.png, .pdf, .svg).
⬇ "dos_cdse.png" = Output filename. Use .pdf for vector / publication; .png for previews.
⬇ dpi=200 = Dots per inch. 200 is a good compromise — sharp on screen, file size still reasonable. 300+ for camera-ready manuscripts.
⬇ bbox_inches="tight" = Trim whitespace and ensure tick labels / legend / axis labels are not clipped. Skipping this is the #1 cause of mysteriously chopped DOS plots.
8 lines without explanation
1from pymatgen.io.vasp.outputs import Vasprun
2from pymatgen.electronic_structure.core import OrbitalType
3from pymatgen.core.periodic_table import Element
4from pymatgen.electronic_structure.plotter import DosPlotter
5import matplotlib.pyplot as plt
6
7vr = Vasprun("vasprun.xml", parse_dos=True)
8
9dos = vr.complete_dos
10e_fermi = dos.efermi
11print(f"Fermi level: {e_fermi:.3f} eV")
12
13plotter = DosPlotter(zero_at_efermi=True, stack=False, sigma=0.05)
14
15plotter.add_dos("Total DOS", dos)
16
17element_dos = dos.get_element_dos()
18for element, edos in element_dos.items():
19    plotter.add_dos(f"{element.symbol} (all orbitals)", edos)
20
21cd_spd = dos.get_element_spd_dos(Element("Cd"))
22plotter.add_dos("Cd 5s", cd_spd[OrbitalType.s])
23
24se_spd = dos.get_element_spd_dos(Element("Se"))
25plotter.add_dos("Se 4p", se_spd[OrbitalType.p])
26
27ax = plotter.get_plot(xlim=(-6, 6), ylim=(0, 18))
28plt.axvline(0, color="black", linestyle="--", linewidth=0.6)
29plt.savefig("dos_cdse.png", dpi=200, bbox_inches="tight")

Run it, open dos_cdse.png, and you should see the canonical CdSe story drawn for you: a clean gap at E=0E = 0, a dominant Se 4p shoulder just below zero, a Cd 5s peak just above zero, and a sharp narrow Cd 4d band a few eV below the VBM. Every feature in that plot is a sentence in the electronic-structure story of the material.


Summary

  • The density of states g(E)=1VnBZδ(EEn(k))d3k(2π)3g(E) = \frac{1}{V}\sum_n \int_\text{BZ} \delta(E-E_n(\mathbf{k}))\,\frac{d^3k}{(2\pi)^3} counts how many electronic states sit at each energy. It is what experimental probes (heat capacity, photoemission, optics) actually see — the lab's view of the band structure.
  • Equivalently, g(E)=nSn(E)dS/[(2π)3kEn]g(E) = \sum_n \int_{S_n(E)} dS / [(2\pi)^3 |\nabla_{\mathbf{k}} E_n|]: a constant-energy-surface integral weighted by the inverse of the band gradient. Flat bands → big DOS, steep bands → small DOS.
  • For free electrons, the DOS is 1/E1/\sqrt{E} (1-D), const\text{const} (2-D), and E\sqrt{E} (3-D) — the dimensionality fingerprint that quantum-confined nanostructures inherit.
  • Van Hove singularities are the kinks, peaks, and divergences in g(E)g(E) at critical points of En(k)E_n(\mathbf{k}). The 2-D logarithmic peak (saddle point) is responsible for graphene's famous van Hove singularity and the magic-angle bilayer flat-band physics.
  • Projected DOS decomposes g(E)g(E) by atom and angular momentum. For CdSe: VBM is Se 4p, CBM is Cd 5s, semicore Cd 4d sits ~7 eV below VBM. Mn dopants add mid-gap d-states.
  • In VASP: do an SCF with ISMEAR=0\texttt{ISMEAR}=0, then a non-SCF run with ICHARG=11\texttt{ICHARG}=11, ISMEAR=5\texttt{ISMEAR}=-5 (tetrahedron), LORBIT=11\texttt{LORBIT}=11, and a denser k-grid. Plot with five lines of pymatgen.
Section 5.2 Core Insight
"The density of states is the band structure with the geometry integrated out — what the experiment sees, weighted everywhere by the inverse of how fast the band runs."
Coming next: Section 5.3 — Metals, Semiconductors, and Insulators — where the integral of g(E)g(E) up to EFE_F tells us whether the material conducts, insulates, or sits on the boundary between the two.
Loading comments...