Chapter 3
18 min read
Section 23 of 70

The First Brillouin Zone

Reciprocal Space and Diffraction

Learning Objectives

Section 3.2 gave us the reciprocal lattice — an infinite set of vectors G\mathbf{G} that "the crystal cannot tell from zero". But infinity is not a place we can compute in. Every plane wave eikre^{i\mathbf{k}\cdot\mathbf{r}} with k\mathbf{k} shifted by G\mathbf{G} describes the same physical state in the crystal, so reciprocal space is wildly over-counted. The first Brillouin zone is the smallest, most symmetric region that gives every physical wavevector exactly once. By the end of this section you should be able to:

  1. State the BZ defining property in one line: every k\mathbf{k} in reciprocal space differs from a unique k\mathbf{k}' in the BZ by a reciprocal-lattice vector G\mathbf{G}.
  2. Construct the BZ geometrically using the Wigner–Seitz recipe — perpendicular bisectors of the shortest G\mathbf{G} vectors enclose it.
  3. Identify the BZ shape for the three cubic Bravais lattices: a cube for SC, a rhombic dodecahedron for BCC (12 rhombic faces), and a truncated octahedron for FCC (8 hexagons + 6 squares).
  4. Locate and name the high-symmetry points Γ,X,L,K,W,U\Gamma, X, L, K, W, U on the FCC BZ — the same points you will read off every CdSe / GaAs / Si band-structure plot.
  5. Explain why a finite piece of crystal — e.g. a periodic supercell or a real CdSe quantum dot — corresponds to a discrete grid of k\mathbf{k} inside the BZ, and how time-reversal symmetry plus crystal symmetry collapses that grid into the still smaller irreducible Brillouin zone.
  6. Construct the FCC BZ in six lines of NumPy + SciPy, verify it numerically, and recognise the same numbers in the printout from vasp_std.
One-line preview: the first Brillouin zone is the Wigner–Seitz cell of the reciprocal lattice. Every fact about band structures, k-point sampling, and selection rules in the rest of this book lives inside it.

Anatomy of a k-Point — What It Is, Why We Need It

Before we build a cage in reciprocal space, we have to look one of its inhabitants in the eye. Every later section — high-symmetry points, Bloch's theorem, k-grids, Monkhorst–Pack, the entire KPOINTS file in VASP — revolves around a single object: a k-point. So what is one?

The one-line answer

A k-point is just a vector k\mathbf{k} in reciprocal space — a point you can put your finger on inside the Brillouin zone. Three real numbers (or two, in 2D), nothing more. But that single vector is doing the work of an infinite number of electrons, so it pays to understand it from three angles.

Pocket definition. A k-point is a wavevector that labels one Bloch state of the crystal. Pick a k, and you have selected one specific way the electron wavefunction can repeat itself from cell to cell.

Three ways to read a single k

The same vector k\mathbf{k} wears three hats. Senior people switch between them mid-sentence, and you should too.

  1. k as a label. Bloch's theorem (Section 3.5) will say every electron state in a crystal carries a wavevector k\mathbf{k} the way every passenger on a plane carries a seat number. Two electrons with the same band index but different k\mathbf{k} are different states. So k\mathbf{k} is the name tag of a quantum state.
  2. k as a wavelength + direction. The plane-wave envelope of that state is eikre^{i\mathbf{k}\cdot\mathbf{r}}. Its wavelength is λ=2π/k\lambda = 2\pi / |\mathbf{k}| and it travels in the direction of k\mathbf{k}. So k\mathbf{k} is geometry — how many oscillations per unit length, and along which axis.
  3. k as cell-to-cell phase. Move from one unit cell to the next along a lattice vector ai\mathbf{a}_i; the wavefunction picks up a pure phase eikaie^{i\mathbf{k}\cdot\mathbf{a}_i}. So k\mathbf{k} is the phase advance per cell. k = 0 means every cell is in phase; k at the zone boundary means neighbouring cells are 180° out of phase.

The unifying picture

All three readings are the same vector. Drag k\mathbf{k} in the widget below and you watch the wavelength change (reading #2), the cell-to-cell phase change (reading #3), and the state being labelled change (reading #1). They are not three things — they are three faces of one thing.

The cell-to-cell phase as a picture

Reading #3 deserves one image before we move on. Take a 1D crystal of spacing aa, watch the sign of Reψ\mathrm{Re}\,\psi at the centre of each repeated cell, and compare the two extremes you can reach inside the first BZ:

k=0\mathbf{k} = 0 — the Γ point
Re ψ(x)+cell 1+cell 2+cell 3+cell 4a

Phase advance per cell ka=0\mathbf{k}\cdot\mathbf{a} = 0; the Bloch phase eika=+1e^{i\mathbf{k}\cdot\mathbf{a}} = +1. The envelope is flat, every atom carries the same sign — the wave is in lock-step with the lattice.

k=π/a\mathbf{k} = \pi/a — the zone boundary (X-like)
Re ψ(x)+cell 1cell 2+cell 3cell 4λ = 2a

Phase advance per cell ka=π\mathbf{k}\cdot\mathbf{a} = \pi; the Bloch phase eika=1e^{i\mathbf{k}\cdot\mathbf{a}} = -1. The wavelength is exactly two cells, so neighbouring atoms flip sign — “as wiggly as the lattice can host.”

Every other k\mathbf{k} in the BZ sits between these two extremes, with an intermediate phase advance per cell — e.g. k=π/(2a)\mathbf{k} = \pi/(2a) shifts each cell by 90° (Bloch phase +i+i). So Γ does not mean “the centre of the crystal.” It means zero phase advance between repeated cells. And the zone boundary is not a barrier in real space — it is the wave pattern beyond which any further wiggling is just an alias of something already inside the BZ.

Why we need k-points at all

A crystal has on the order of 102310^{23} atoms. The Schrödinger equation has, naively, that many coupled equations. Without translational symmetry, this is unsolvable. k-points are how the symmetry pays you back.

Translational symmetry says: if a wavefunction is an eigenstate of the Hamiltonian, it is also an eigenstate of the translation operator T^R\hat{T}_{\mathbf{R}}. The eigenvalues of T^R\hat{T}_{\mathbf{R}} are pure phases eikRe^{i\mathbf{k}\cdot\mathbf{R}} — one phase per cell for each k\mathbf{k}. So the impossible 102310^{23}-particle problem decomposes into one independent problem per k\mathbf{k}, each living inside a single unit cell. Pick a k, solve a tiny problem; sweep k across the BZ, recover the whole solid.

Why we need to know it

Once you accept the decomposition, every property of the solid is some integral over k: total energy, charge density, density of states, optical absorption, thermal conductivity. Knowing what one k-point means is the prerequisite for understanding what it means to sample them (Sections 3.8–3.9). VASP's KPOINTS file is, in the end, just a list of these vectors with weights.

What a k-point tells you about the solid

A k by itself is a label, not yet a physical answer. But once you ask the Hamiltonian about it, it returns four pieces of crystal physics:

Quantity at k\mathbf{k}What it tells youWhere you see it
Band energies En(k)E_{n}(\mathbf{k})Allowed electron energies for that wavevector. Many bands per k\mathbf{k}, indexed by nn.Band-structure plots; gaps; effective masses
Group velocity kEn(k)\nabla_{\mathbf{k}}\,E_{n}(\mathbf{k})How fast (and in which direction) an electron in that state propagates.Conductivity, optical response, mobility
Bloch state ψn,k(r)\psi_{n,\mathbf{k}}(\mathbf{r})The actual wavefunction — its lobes, nodes, and orbital character on each atom.Charge density, bonding analysis, COHP
Symmetry irrep at k\mathbf{k}Which symmetries fix this k\mathbf{k}, which swap it. Decides which transitions are allowed.Selection rules; degeneracy at Γ,X,L\Gamma, X, L

The intuition in one sentence

A k-point is a phase-pattern across the lattice; a Bloch state at that k is what fits that phase pattern; the band energy at that k is what it costs the crystal to host that fit.

Interactive — one k, one wave

Drag the purple dot inside the square Brillouin zone. The right panel shows the plane wave cos(kr)\cos(\mathbf{k}\cdot\mathbf{r}) sampled on a square atomic lattice with lattice constant a=1a = 1. Watch four things change at once:

  • The wavelength λ=2π/k\lambda = 2\pi / |\mathbf{k}| shrinks as you push k away from Γ.
  • The direction of the wavefronts rotates with the direction of k.
  • The cell-to-cell phase Δφ=ka\Delta\varphi = \mathbf{k}\cdot\mathbf{a} tells you how much the wave twists per atom.
  • At the zone boundary (X = (π, 0), M = (π, π)) neighbouring atoms flip sign — the wave is “as wiggly as the lattice will allow”.
Loading k-point widget…
Try this: click Γ, then ¼·X, then ½·X, then X. Watch the atoms march from “all red” (in phase) to “alternating red/blue” (anti-phase). That is the entire span of distinct phase patterns the lattice can carry along the x-axis — everything beyond X is just an alias of something already inside. That redundancy is exactly what the next subsection cures.

From 2D to 3D

Real crystals are three-dimensional, and so are their k-points. The square BZ above becomes a polyhedron — a cube for SC, a rhombic dodecahedron for BCC, a truncated octahedron for FCC. The meaning of a single k-point is unchanged: it is still a wavevector, still a phase-per-cell, still a label on a Bloch state. The geometry is the only thing that gets richer. We meet the FCC version in detail later in this section (“The FCC Brillouin Zone in Detail”); for now, just hold the picture: a k-point is a tiny purple dot you can drop anywhere inside the cage we are about to build.

Look-ahead — supercells, k-meshes, and your Mn-doped calculations

Real space and reciprocal space have inverse spacings: a large real-space cell has a small Brillouin zone. So a 64- or 128-atom Mn-doped AgZnInS2 supercell needs far fewer k-points than the parent primitive cell — sometimes Γ-only is honest, sometimes 2×2×2 is the sweet spot, but you must always confirm by convergence testing the quantity you actually care about (gap, magnetic moment, Mn-d level). FAQ items 4 and 7 below unpack the rule and the recipe; Section 3.9 (Monkhorst–Pack) gives the formal mesh definition.

Three things readers usually mis-learn

  • k is not momentum. It is crystal momentum. They behave similarly inside the BZ but differ by reciprocal-lattice vectors G\mathbf{G} — we will spend the next subsection on exactly that.
  • k is continuous, not a grid. The “grid of k-points” you set in a VASP calculation is a numerical sample of the continuous BZ, not the BZ itself.
  • One k, many bands. A k-point does not pick a state by itself — it labels a family of states (one per band). Always specify both n and k.

Conceptual Deep Dives — Real Space, Reciprocal Space, k-Points

Before we tighten the formal “cage” argument in the next section, it is worth pausing to answer the questions every student asks the first time they see a Brillouin zone. These panels unpack each idea slowly — expand the ones you need, skip the ones already obvious. They are deliberately repetitive: each angle of attack reinforces the others.

Read this first. A real-space point tells where something is. A k-point tells how a wave changes phase from one repeated unit cell to the next. That single sentence dissolves about 80 % of the confusion below.

Carry these takeaways into the rest of the chapter. A k-point is a wave-pattern label, not a position. The Brillouin zone is the unique-pattern map. Real space tells you the atomic skeleton; reciprocal space tells you what waves can do on that skeleton. Everything VASP outputs — band structure, DOS, gap, forces, magnetic moment — is an integral or a slice of physics living inside this cage.

Interactive — k-Points as Wave-Pattern Labels

Time to drive the abstraction. The widget below ties together every panel in the FAQ above: a real-space chain of unit cells, an animated wave, a slider for k\mathbf{k}, the Brillouin-zone axis, a Monkhorst–Pack-style mesh sampler, a conceptual E(k)E(\mathbf{k}) plot, and a toggle between delocalised and localised states. Every panel responds to the same k\mathbf{k}, so when you slide it, you watch the wavelength, cell-to-cell phase, BZ marker, mesh sample, band-structure dot, and (in localised mode) flat-band signature change simultaneously.

  • Section 1 — the real-space crystal. Cell tints follow Reψ\mathrm{Re}\,\psi at the cell centre, so “phase per cell” becomes a colour you can see. Slide aa to widen or narrow the cells.
  • Section 2 — the k\mathbf{k} slider plus a phasor wheel showing eikae^{i\,\mathbf{k}\cdot\mathbf{a}}. Click Γ, then π/(2a), then X, and watch the wheel rotate from +1+1 through +i+i to 1-1.
  • Section 3 — the reciprocal-space axis. The marker is not an atom — it is selecting a wave pattern.
  • Section 4 — pick a mesh size N{1,3,5,7,9}N \in \{1,3,5,7,9\}. Each mesh point shows a thumbnail of the wave VASP would solve at that k\mathbf{k}.
  • Section 5 — conceptual E(k)=Ck2E(\mathbf{k})=C\mathbf{k}^2 with an optional stylised gap at the BZ boundary. Drag k\mathbf{k} and watch the dot trace the band.
  • Section 6 — toggle delocalised vs localised. The localised mode renders an Mn-like impurity wavepacket and flattens E(k)E(\mathbf{k}) — the textbook flat-band signature.
Loading k-point wave-pattern visualizer…
Suggested 60-second tour. Start at Γ (Section 2 preset) and notice every cell tint matches — all cells in phase. Click π/(2a): each cell now shifts by 90°, and the phasor wheel points to +i+i. Click X: cells alternate red/blue, the phasor lands on 1-1, and the wave is “as wiggly as the lattice allows.” Now flip Section 6 to localised — the wave collapses around one atom and Section 5's band goes flat. That single tour is the entire idea.

Interactive — Same Idea in 3D (Real-Space Wave Pattern)

The 1D widget made one thing visceral: a k-point is a phase pattern threaded through repeated cells. Real crystals are 3D, so let's lift the same picture into three dimensions. Below is a 3×3×3 simple-cubic lattice (27 atoms). Each atom's colour and size are driven live by Reψk(r,t)=cos(krωt)\mathrm{Re}\,\psi_{\mathbf{k}}(\mathbf{r}, t) = \cos(\mathbf{k}\cdot\mathbf{r} - \omega t). Three translucent violet sheets are surfaces of constant phase — they drift along k\mathbf{k} with phase velocity ω/k\omega/|\mathbf{k}|, separated by the wavelength λ=2π/k\lambda = 2\pi/|\mathbf{k}|.

  • Γ presetk=0\mathbf{k}=0. Every atom turns the same colour at the same instant: zero spatial wave, just a global phase oscillation. Wavelength is infinite, so the planes vanish.
  • X preset k=(π/a,0,0)\mathbf{k}=(\pi/a, 0, 0). Atoms alternate sign along the xx-axis but stay synchronised in y,zy, z. The phase planes are perpendicular to x^\hat{x} and slide along it — this is the 3D version of a 1D zone-boundary wave.
  • M preset k=(π/a,π/a,0)\mathbf{k}=(\pi/a, \pi/a, 0). Diagonal-stripe pattern in thexyxy-plane, synchronised along zz.
  • R preset k=(π/a,π/a,π/a)\mathbf{k}=(\pi/a, \pi/a, \pi/a). Full 3D checkerboard — every nearest neighbour has the opposite sign, in every direction. This is the corner of the cubic BZ — nothing wigglier exists.
  • Anywhere in between. Drag any of the three sliders (kx,ky,kz)(k_x, k_y, k_z) independently. Watch wavefronts tilt, atoms march out of sync, and λ\lambda grow as you shrink k|\mathbf{k}|.
Loading 3D visualisation…

Connecting the two widgets

The 1D widget and this 3D widget are showing the same physics. Each row of atoms along x^\hat{x} in this 3D view is doing exactly what the 1D chain does for some kxk_x; the 3D picture just lets the wave also twist along y^\hat{y} and z^\hat{z}. A Bloch state in any real crystal is one of these patterns, no more and no less.

What this widget is not

  • The wave shown is the plane-wave envelope eikre^{i\mathbf{k}\cdot\mathbf{r}}, not a full Bloch state. A real Bloch state multiplies this by a periodic function unk(r)u_{n\mathbf{k}}(\mathbf{r}) that lives inside one cell. The cell-to-cell phase pattern, however, is exactly what the widget shows.
  • ω\omega here is a fictitious animation frequency for visual rhythm only — do not read off a dispersion from it. Real ω(k)\omega(\mathbf{k}) comes from the band structureEn(k)=ωn(k)E_n(\mathbf{k}) = \hbar\omega_n(\mathbf{k}), which is what VASP computes.

Interactive — Sampling the 3D Brillouin Zone (Monkhorst–Pack)

One k-point is a single wave pattern. A real DFT calculation needs many of them — because every observable (total energy, charge density, band structure, DOS) is an integral over the Brillouin zone, and a finite computer needs a finite sample. The widget below shows what that sample actually looks like for the simplest case: a cubic Brillouin zone (the BZ of a simple-cubic crystal), sampled by the standard Monkhorst–Pack recipe.

Click an NN button to set the mesh density N×N×NN \times N \times N. Each dot is one wave-pattern label k\mathbf{k}; dots that share a colour are symmetry-equivalent under the cubic point group OhO_h — they describe the same physics and only need to be computed once. The translucent green wedge is the irreducible Brillouin zone (IBZ): the tetrahedron with corners Γ,X,M,R\Gamma, X, M, R that VASP actually does the work in.

  • Raw count — the number of dots, equal to N3N^3. This is what KPOINTS specifies.
  • IBZ count — the number of unique colours, equal to the number of lines VASP writes to IBZKPT. This is what determines compute cost.
  • Speed-up — the ratio. Approaches Oh=48|O_h| = 48 as the mesh grows, because most points have full stars (8 sign-flips × 6 axis-permutations). Coarse meshes have lower speed-ups because more points sit on symmetry planes (smaller stars).
Loading 3D visualisation…

Try this

Switch to N=4N = 4 and notice the symmetry classes: 8 corner dots all share one colour (the (±3/8,±3/8,±3/8)(\pm 3/8, \pm 3/8, \pm 3/8) star, multiplicity 8); 24 dots share another (the (3/8,3/8,1/8)(3/8, 3/8, 1/8) star, full general-position multiplicity 24). The full 43=644^3 = 64 raw points fold to 4 IBZ points — a 16× speed-up. Push NN to 8 and watch the speed-up climb toward 30× as more interior points reach full OhO_h orbits.

The picture you should leave with

VASP's KPOINTS file picks the dot pattern; VASP's IBZKPT file is the green-wedge subset (with weights equal to each star's multiplicity). Every BZ integral becomes a weighted sum over those green dots. That is what k-point sampling means in practice.

From cubic to FCC, BCC, hex, …

The BZ shown here is the cube of a simple-cubic crystal — chosen because the symmetry argument is easiest to see. Real materials of interest in this book (CdSe and AgZnInS2 in zinc-blende form) have the FCC truncated-octahedron BZ shown later in “The FCC Brillouin Zone in Detail.” The Monkhorst–Pack recipe works the same way: a regular grid in fractional reciprocal coordinates, folded by whatever point group the crystal has. The cubic case is just the cleanest demo.

The Redundancy of k — Why We Need a Cage

Bloch's theorem (Section 3.5 will give it in full) tells us that every electron in a crystal has a wavefunction of the form

ψk(r)  =  eikruk(r),\psi_{\mathbf{k}}(\mathbf{r}) \;=\; e^{i\mathbf{k}\cdot\mathbf{r}}\, u_{\mathbf{k}}(\mathbf{r}),

where uku_{\mathbf{k}} is periodic with the lattice. Now ask the obvious question: what happens if we replace k\mathbf{k} with k+G\mathbf{k} + \mathbf{G}? Compute:

ψk+G(r)=ei(k+G)ruk+G(r)=eikr[eiGruk+G(r)].\psi_{\mathbf{k}+\mathbf{G}}(\mathbf{r}) = e^{i(\mathbf{k}+\mathbf{G})\cdot\mathbf{r}}\,u_{\mathbf{k}+\mathbf{G}}(\mathbf{r}) = e^{i\mathbf{k}\cdot\mathbf{r}}\,\bigl[e^{i\mathbf{G}\cdot\mathbf{r}}u_{\mathbf{k}+\mathbf{G}}(\mathbf{r})\bigr].

The factor in square brackets is itself periodic on the lattice, because eiGre^{i\mathbf{G}\cdot\mathbf{r}} is — that is exactly what defined G\mathbf{G} in Section 3.2. So we can absorb it into a redefined uu and end with a wavefunction of exactly the same Bloch form, just with a different periodic part. Bottom line:

  k  and  k+G  describe the same physical state.  \boxed{\;\mathbf{k} \;\text{and}\; \mathbf{k} + \mathbf{G} \;\text{describe the same physical state.}\;}

What this redundancy looks like in practice

You have a valid band-structure result at k=(3.7,0.2,5.1)A˚1\mathbf{k} = (3.7, -0.2, 5.1)\,\text{Å}^{-1}. Add  G1=(1.03,1.03,1.03)A˚1\;\mathbf{G}_1 = (-1.03, 1.03, 1.03)\,\text{Å}^{-1} — the eigenvalues are identical. Add 100G1100\,\mathbf{G}_1 — still identical. There are infinitely many representatives of every physical state, and we have to choose one. The BZ is the canonical choice.

The challenge: out of the infinitely many points equivalent to a given physical state, pick exactly one. Mathematicians call any region that does this a fundamental domain. Two natural candidates compete:

  1. The reciprocal primitive cell — a parallelepiped with vertices at integer combinations of b1,b2,b3\mathbf{b}_1, \mathbf{b}_2, \mathbf{b}_3. Easy to define, but skewed and ugly: it has none of the rotational symmetries of the lattice.
  2. The Wigner–Seitz cell of the reciprocal lattice — the set of points closer to the origin than to any other reciprocal lattice point. Geometric, lattice-symmetric, and is what every textbook and every code calls "the first Brillouin zone".

Both have the same volume V=(2π)3/VV^* = (2\pi)^3/V (we proved this in Section 3.2). But the second one inherits every point-group symmetry of the crystal — cubic crystals get cubic-symmetric BZs, hexagonal crystals get hexagonal-symmetric BZs. That symmetry is a gift from the gods: it lets us shrink the integration region for any physical observable by the order of the point group, often a factor of 24 or 48.


What Is a Fundamental Domain?

A region DR3D \subset \mathbb{R}^3 is a fundamental domain of a lattice Λ\Lambda when:

  • Coverage. Every point in space is equivalent to some point in DD under translation by Λ\Lambda: for every k\mathbf{k} there is a GΛ\mathbf{G} \in \Lambda with kGD\mathbf{k} - \mathbf{G} \in D.
  • No double-counting. Distinct interior points of DD are never equivalent. Boundary points may be — but together they have measure zero, so they don't affect any integral.

These two conditions force vol(D)=V\text{vol}(D) = V^* exactly. For the reciprocal lattice in 3-D, infinitely many regions qualify — a slanted parallelepiped, a slab, a starfish, a Voronoi cell, anything that tiles space by translation. The first Brillouin zone is the special one:

BZ1  =  {kR3    kkG  for every GΛ{0}}.\mathrm{BZ}_1 \;=\; \bigl\{\, \mathbf{k} \in \mathbb{R}^3 \;\bigm|\; |\mathbf{k}| \leq |\mathbf{k} - \mathbf{G}| \;\text{for every } \mathbf{G} \in \Lambda^*\setminus\{0\} \,\bigr\}.

Read it slowly: k\mathbf{k} belongs to the BZ if it is at least as close to the origin as it is to any other reciprocal-lattice point. Geometrically: everyone wants to live closer to home than to the neighbours.

The same construction in real space

We have already used this idea once! In Chapter 1 the Wigner–Seitz cell of a real-space lattice was the set of points closer to one chosen lattice site than to any neighbour. The first BZ is literally the same recipe applied to the reciprocal lattice. The only difference is the units: real-space WS lives in Å, BZ lives in Å⁻¹.


The Wigner–Seitz Recipe

Here is the entire construction in four sentences:

  1. Place the origin in reciprocal space.
  2. Draw straight lines from the origin to every reciprocal-lattice point.
  3. Bisect each line with a plane perpendicular to it (the "Bragg plane" — we will see why in Section 3.6).
  4. The smallest region around the origin enclosed by these planes is the first Brillouin zone.

Mathematically, every bisector plane is {k:kG^=G/2}\{\mathbf{k} : \mathbf{k}\cdot\hat{\mathbf{G}} = |\mathbf{G}|/2\} — the locus of points equidistant from 00 and G\mathbf{G}. The half-space on the origin's side is the set of points closer to 00 than to G\mathbf{G}. The intersection of all such half-spaces is a convex polyhedron centred at the origin — and that polyhedron is the BZ.

A pleasant observation

Only the shortest reciprocal-lattice vectors actually contribute faces. A planet far away from the origin has its bisector plane far away too, well outside the cell carved by closer neighbours. For FCC reciprocal (= BCC), the 12 closest neighbours and the next 6 are enough to enclose the BZ; further neighbours are redundant. This is why the BZ has a finite, small number of faces (8 + 6 = 14 for FCC) even though the lattice is infinite.


Build It in 2D — Step by Step

Before we step into 3D, let's see the construction unfold in 2D where we can see every line. The viewer below walks you through the four steps for square, rectangular, hexagonal, and oblique 2-D lattices. It applies to either real-space Wigner–Seitz cells (used in Chapter 1) or reciprocal-space Brillouin zones — the geometry is identical, only the units differ.

Wigner-Seitz Cell Construction

Step through the geometric recipe for constructing the WS cell

The WS cell is a square — it has the full 4-fold symmetry (C₄v).
O

Three habits to take away from playing with the controls:

  • Symmetry inheritance. The square lattice produces a square BZ; the hexagonal lattice produces a regular hexagon. The BZ has every rotation, reflection, and inversion that the lattice has, plus inversion through the origin (always — we'll see why under "time reversal" below).
  • Only nearest neighbours matter. Toggle the steps and watch: the WS cell stops shrinking after the first ring of bisectors. Distant lattice points contribute redundant constraints.
  • Skewing the lattice skews the BZ. An oblique 2-D lattice gives an irregular hexagon — yes, hexagon: a generic 2D lattice has 6 nearest neighbours of two distinct kinds, hence 6 BZ edges of two distinct lengths.

Step Up to 3D — SC, BCC, FCC

In 3D the BZ is a polyhedron, not a polygon. Three cases own everything you will encounter for cubic crystals (and CdSe in zinc-blende form is one of them):

Real latticeReciprocal latticeBZ shape# faces# vertices
Simple cubic (SC)Simple cubicCube6 squares8
Body-centred cubic (BCC)FCCRhombic dodecahedron12 rhombi14
Face-centred cubic (FCC)BCCTruncated octahedron8 hex + 6 squares24

The viewer below lets you step through the construction (lattice points → lines to neighbours → bisector planes → final cell) for each of the three lattices. Bear in mind: pick the lattice whose reciprocal shape you want — to see the FCC BZ, click BCC, because the FCC reciprocal is BCC and the WS cell of BCC is the truncated octahedron.

Loading 3D visualisation…

That last sentence is worth re-reading. The component shows the WS cell of the chosen direct lattice. Cross-applying the FCC↔BCC duality from Section 3.2:

  • BZ of a real-space FCC crystal — like CdSe, Si, Cu, Au — is the WS cell of BCC: the truncated octahedron.
  • BZ of a real-space BCC crystal — like Fe, Cr, Mo, Na — is the WS cell of FCC: the rhombic dodecahedron.
  • BZ of a real-space SC crystal (rare in nature, common in textbook exercises): a cube of side 2π/a2\pi/a, just like the reciprocal lattice itself.

Beginner trap

Almost every confusion about Brillouin zones reduces to forgetting which lattice you are taking the WS cell of. Mantra: BZ = WS of the reciprocal. If your crystal is FCC in real space, the BZ is built around the BCC reciprocal points.


The FCC Brillouin Zone in Detail

Of all the BZ shapes, the FCC truncated octahedron is the one you will see most. CdSe (zinc-blende), GaAs, Si, Ge, diamond, Cu, Al, all noble metals, all platinum-group metals, the entire fluorite family — every electronic-structure plot in those papers happens inside this exact polyhedron. Memorising its landmarks pays off forever.

With reciprocal-space coordinates in units of 2π/a2\pi/a (where aa is the conventional cube edge), the high-symmetry points are:

SymbolCoordinates (2π/a)Location on BZMultiplicity
Γ (Gamma)(0, 0, 0)Zone centre1
X(1, 0, 0)Centre of square face6
L(½, ½, ½)Centre of hexagonal face8
W(1, ½, 0)Vertex (1 square + 2 hexes meet)24
K(¾, ¾, 0)Midpoint of hex–hex edge12
U(1, ¼, ¼)Midpoint of hex–square edge24

Spin the viewer below, click each label on the right, and watch the dot move:

Loading 3D visualisation…

The amber path drawn on the BZ — ΓXWLΓK\Gamma \to X \to W \to L \to \Gamma \to K — is the standard band-structure path for FCC crystals (Setyawan&Curtarolo, Comp. Mat. Sci. 2010). Every time you see "the band gap of CdSe is direct at Γ\Gamma", this is the path the bands were plotted along; Γ\Gamma is its first stop.

Why these specific points?

They are the points that map to themselves under the largest little groups of OhO_h — the cubic point group of the FCC BZ. Bands must split, cross, or touch in symmetry-prescribed ways at exactly these points (this is what Chapter 2's character tables predict). So band structures are required by group theory to expose their interesting features at Γ,X,L,K,W,U\Gamma, X, L, K, W, U — and a path that touches them samples the entire physically meaningful spectrum.

Reading the geometry off the polyhedron

Three observations worth pinning down before we move on:

  1. The square faces are perpendicular to x^,y^,z^\hat{\mathbf{x}}, \hat{\mathbf{y}}, \hat{\mathbf{z}} and centred at X=(±1,0,0)2π/aX = (\pm 1, 0, 0) \cdot 2\pi/a and permutations — the 6 next-nearest reciprocal-lattice neighbours, at distance 22π/a2 \cdot 2\pi/a.
  2. The hexagonal faces are perpendicular to (±1,±1,±1)(\pm 1, \pm 1, \pm 1) directions and centred at the 8 L points — the 8 nearest reciprocal-lattice neighbours, at distance 32π/a\sqrt{3} \cdot 2\pi/a. The hexagons are regular: side length 2/22π/a\sqrt{2}/2 \cdot 2\pi/a.
  3. Each W vertex sits at the meeting of one square face and two hexagonal faces. The 24 W's are equivalent under OhO_h; they are where bands typically show their saddle-point Van Hove singularities.

The BZ Shape Duality Table

Compiling the FCC↔BCC dualities from Section 3.2 with the WS construction we just did, every 3-D Bravais lattice has a known BZ shape:

Real-space crystalBZ shapeFamous example
Simple cubic (P)CubePolonium
Body-centred cubic (I)Rhombic dodecahedronα-Fe, Cr, Mo, W, Na
Face-centred cubic (F)Truncated octahedronCu, Al, Ag, Au, Ni; CdSe (zinc-blende)
Hexagonal (P)Hexagonal prismMg, Zn, graphene; CdSe (wurtzite)
Tetragonal (P)Square prismβ-Sn, rutile TiO₂
Orthorhombic (P)Rectangular boxα-S, ZrSiO₄

CdSe lives twice in this table

The bulk CdSe of our target lives in two crystal forms: the cubic zinc-blende (FCC, truncated-octahedron BZ) and the hexagonal wurtzite (hex prism BZ). Most VASP studies of CdSe quantum dots use zinc-blende for simplicity. We will too, in Chapter 6.


The Irreducible BZ — Symmetry's Last Cut

Even the BZ over-counts. Reason: every crystal has time-reversal symmetry (in non-magnetic systems) and a point group of rotations and reflections. Time reversal sends kk\mathbf{k} \to -\mathbf{k} while leaving energies unchanged, so En(k)=En(k)E_n(\mathbf{k}) = E_n(-\mathbf{k}) for every band nn. Combined with the crystal point group, the BZ collapses by a factor equal to the order of the group:

vol(IBZ)  =  vol(BZ)Oh{time reversal}  =  V48    for cubic FCC.\text{vol}(\text{IBZ}) \;=\; \frac{\text{vol}(\mathrm{BZ})}{|\,O_h \cup \{\text{time reversal}\}\,|} \;=\; \frac{V^*}{48} \;\;\text{for cubic FCC}.

The result is a small wedge called the irreducible Brillouin zone (IBZ). For the FCC truncated octahedron the IBZ is the tetrahedron with corners Γ,X,L,W\Gamma, X, L, W. Computing any Brillouin-zone-averaged property — total energy, density of states, optical absorption — only requires sampling k\mathbf{k}-points inside the IBZ; the rest is filled in by symmetry. In practice this is the difference between a 4-hour and a 4-minute calculation.

Where this lands in VASP

When VASP reads your KPOINTS file it generates a regular k-mesh inside the BZ, then folds it down to the IBZ using the symmetries it detected from POSCAR. The compressed list, with multiplicity weights, lives in the file IBZKPT. A 12×12×12 mesh on the FCC BZ (1728 raw points) often becomes only ~72 IBZ points — a 24× speed-up. The factor varies; check IBZKPT for your structure.


Brillouin Zone in VASP — KPOINTS, IBZKPT, and KPOINTS_OPT

Open any VASP input directory and you will find a KPOINTS file. It tells VASP how to discretise the BZ:

📝text
1Automatic mesh
20
3Monkhorst-Pack
412 12 12
50  0  0

Five lines. Line 1 is a comment. Line 2 ("0") means "generate the mesh automatically". Line 3 sets the scheme; Monkhorst–Pack is the textbook even-grid sampler we'll meet in Section 3.9. Line 4 is the mesh density along the three reciprocal lattice directions. Line 5 is an optional shift (kept at zero for now).

After running VASP you will find a companion file IBZKPT with a header like:

📝text
1Automatically generated mesh
2   72
3Reciprocal lattice
4  0.00000000  0.00000000  0.00000000      1
5  0.08333333  0.00000000  0.00000000      8
6  0.16666667  0.00000000  0.00000000      8
7  0.25000000  0.00000000  0.00000000      8
8  ...

Three things to read off:

  • Number 72. Out of 12×12×12 = 1728 raw mesh points, only 72 are unique up to OhO_h + time reversal. That ratio 1728/72 = 24 is the order of the cubic group, exactly as expected.
  • The k-vectors. They are listed in fractional reciprocal coordinates — i.e., as multiples of b1,b2,b3\mathbf{b}_1, \mathbf{b}_2, \mathbf{b}_3, not in Å⁻¹. Multiply by BB to convert.
  • The integer weights. Each IBZ point stands for a number of equivalent BZ points (its "star") — that's the integer in the last column. They sum to 1728. When VASP averages anything over the BZ, it uses these weights.

For band-structure calculations you turn off the automatic mesh and instead provide a line-mode KPOINTS that walks along the high-symmetry path we plotted on the truncated octahedron above:

📝text
1k-points along high symmetry lines
230
3Line-mode
4Reciprocal
5  0.0   0.0   0.0   ! Γ
6  0.5   0.0   0.5   ! X
7
8  0.5   0.0   0.5   ! X
9  0.5   0.25  0.75  ! W
10
11  0.5   0.25  0.75  ! W
12  0.5   0.5   0.5   ! L
13
14  0.5   0.5   0.5   ! L
15  0.0   0.0   0.0   ! Γ
16
17  0.0   0.0   0.0   ! Γ
18  0.375 0.375 0.75  ! K

VASP will sample 30 points along each segment, evaluate the bands at every k, and concatenate them into the EIGENVAL file. That data, plotted with energy on the y-axis and k on the x-axis along the path, is your band-structure plot. Every band-structure figure in this book — and in the literature — is produced from exactly this kind of file.

Coordinates: fractional vs. Cartesian

The block above starts with Reciprocal, so the numbers are in fractional (b1,b2,b3)(\mathbf{b}_1, \mathbf{b}_2, \mathbf{b}_3) coordinates. The same path written with Cartesian would multiply each row by BB, giving Å⁻¹. Mixing them up is the most common KPOINTS mistake new users make, especially because VASP refuses to warn you — you simply get a path through the wrong region of k-space and a nonsensical band structure.


Constructing the BZ in Python — Voronoi in 6 Lines

Theory and pictures done — now build the FCC BZ from scratch, numerically. The program below uses SciPy's Voronoi class to perform the Wigner–Seitz construction on the reciprocal lattice and prints two cross-checks: the W-vertex count (must be 24) and the reciprocal-cell volume (must equal (2π)3/V(2\pi)^3/V). Click any line on the right to see what every variable holds at that moment of execution.

Build the FCC Brillouin zone — line-by-line execution trace
🐍construct_fcc_bz.py
1import numpy as np

NumPy provides ndarray, the fast N-D array we will use for every lattice vector and matrix operation. All arithmetic below — `@` for matrix multiply, `np.linalg.inv` for matrix inverse, `np.linalg.norm` for vector length — runs as compiled C code, not Python loops.

EXECUTION STATE
📚 numpy = Numerical-computing library. Gives us ndarray, broadcasting, linear algebra (np.linalg), and indexing tricks like np.where.
as np = Universal alias. Lets us write np.array(...) instead of numpy.array(...).
2from scipy.spatial import Voronoi

SciPy's Voronoi class computes the Voronoi tessellation of a set of points: for every input point it builds the convex polyhedral region of space closer to that point than to any other. The Brillouin zone is, by definition, the Voronoi cell of the origin in the reciprocal lattice — so this single import lets us construct any BZ in three lines.

EXECUTION STATE
📚 scipy.spatial.Voronoi = Class that takes an (N, d) array of d-dimensional points and returns the full Voronoi diagram: vertices, regions, ridges. Backed by the QHull library.
→ why we need it = Doing the Voronoi/Wigner–Seitz construction by hand means clipping a polygon by N half-planes (one per neighbour). QHull does this in seconds, robustly, in any dimension.
4Comment — what we are about to build

The constants below set up the FCC primitive lattice for our running CdSe target (lattice parameter a = 6.077 Å is the experimental zinc-blende value). Switching crystal: change just `a` and the matrix template — the rest of the script is lattice-agnostic.

5a = 6.077

Conventional cubic edge length of zinc-blende CdSe in ångström. We'll feed this into the FCC primitive-vector template on the next lines. The whole script scales linearly: doubling `a` halves every reciprocal coordinate.

EXECUTION STATE
a = 6.077 (Å) — experimental lattice constant of cubic CdSe at 300 K
→ unit choice = We work in ångström because that is what VASP's POSCAR uses. 1 Å = 0.1 nm = 10⁻¹⁰ m.
6A = (a/2) * np.array([...])

Builds the 3×3 matrix whose rows are the FCC primitive lattice vectors. The standard FCC primitive basis is a/2·(0,1,1), a/2·(1,0,1), a/2·(1,1,0) — three face-diagonals of the conventional cube. Storing them as ROWS (one per row) is the convention used by VASP's POSCAR and by `np.linalg.inv(A).T` below.

EXECUTION STATE
📚 np.array = Constructs an ndarray from a Python list-of-lists. Shape is inferred — here (3, 3).
(a / 2.0) = Scalar multiplier: 6.077 / 2 = 3.0385. Broadcasts over the entire 3×3 matrix.
→ row layout = Row 0 = a₁, Row 1 = a₂, Row 2 = a₃. Same convention as VASP POSCAR. Some textbooks use columns — be careful when copying formulas.
7Row 0 — a₁ = (a/2)(0, 1, 1)

First primitive vector. Lies in the y–z plane, points to the centre of the +y+z face of the conventional cube. Length = a√2/2 ≈ 4.297 Å.

8Row 1 — a₂ = (a/2)(1, 0, 1)

Second primitive vector. Lies in the x–z plane, points to the centre of the +x+z face.

9Row 2 — a₃ = (a/2)(1, 1, 0)

Third primitive vector. Lies in the x–y plane, points to the centre of the +x+y face.

10Closing bracket — A is a (3, 3) ndarray

After this line, A holds:

EXECUTION STATE
A (3×3) — Å =
         x        y        z
a₁  0.0000  3.0385  3.0385
a₂  3.0385  0.0000  3.0385
a₃  3.0385  3.0385  0.0000
→ V_FCC = det(A) = (a/2)³ · det([[0,1,1],[1,0,1],[1,1,0]]) = 3.0385³ · 2 = 56.10 ų. This is the FCC primitive-cell volume — exactly a³/4 as expected.
12Comment — building the reciprocal lattice

Section 3.2 gave us the closed-form B = 2π·(A⁻¹)ᵀ. One line of NumPy and we have it.

13B = 2 * np.pi * np.linalg.inv(A).T

This is the entire reciprocal-lattice computation. `np.linalg.inv(A)` returns A⁻¹; `.T` transposes it; `2 * np.pi` rescales to the physicist convention. The result B has reciprocal lattice vectors as rows, with units of Å⁻¹.

EXECUTION STATE
📚 np.linalg.inv = Solves A · X = I for X. Internally uses LU decomposition. Cost O(n³). Robust as long as A is non-singular (det ≠ 0).
📚 .T attribute = Returns the transpose — swaps rows and columns. For a (3,3) matrix, axis 0 ↔ axis 1. No data is copied; .T creates a view.
⬇ A (rows = a_i) =
[[0.0000, 3.0385, 3.0385],
 [3.0385, 0.0000, 3.0385],
 [3.0385, 3.0385, 0.0000]]
→ A⁻¹ (cols = a*_i / 2π) =
(1/(2·3.0385)) · [[-1, 1, 1], [1, -1, 1], [1, 1, -1]]
  = [[-0.16455,  0.16455,  0.16455],
     [ 0.16455, -0.16455,  0.16455],
     [ 0.16455,  0.16455, -0.16455]]
→ (A⁻¹)ᵀ × 2π = (2π/a) · [[-1, 1, 1], [1, -1, 1], [1, 1, -1]] 2π/a = 2π / 6.077 ≈ 1.0339 Å⁻¹
⬆ B (rows = b_i, Å⁻¹) =
         k_x       k_y       k_z
b₁  -1.0339   1.0339   1.0339
b₂   1.0339  -1.0339   1.0339
b₃   1.0339   1.0339  -1.0339
→ notice the pattern = These are exactly the BCC primitive vectors, scaled by 2π/a. FCC real-space ↔ BCC reciprocal (the 'BCC surprise' from Section 3.2).
→ duality check = B @ A.T / (2π) should be the 3×3 identity. NumPy: np.allclose(B @ A.T / (2*np.pi), np.eye(3)) → True ✓
15Comment — generating reciprocal-lattice points

We need ENOUGH neighbours so that the Voronoi cell of the origin is fully bounded. ±2 is overkill for FCC (the truncated octahedron only requires the 14 nearest neighbours), but cheap and safe.

16hkl = np.array([(h, k, l) for h in range(-2, 3) ...])

A list comprehension generates every triple (h, k, l) with each in {−2, −1, 0, 1, 2}. That's a 5×5×5 = 125-point grid in integer (Miller-like) coordinates. We will multiply this by B to get the actual reciprocal-space positions.

EXECUTION STATE
📚 list comprehension = Python pattern: [expr for x in iter1 for y in iter2 ...] enumerates the Cartesian product of the iterators. Three nested loops here build all (h, k, l) triples in one expression.
📚 range(-2, 3) = Yields −2, −1, 0, 1, 2 (5 integers). The right end is exclusive — 3 is NOT included. Beginner trap.
⬆ hkl shape = (125, 3) — 125 integer triples
→ first few rows = [[-2, -2, -2], [-2, -2, -1], [-2, -2, 0], [-2, -2, 1], [-2, -2, 2], [-2, -1, -2], ..., [ 0, 0, 0], ← origin, index 62 ..., [ 2, 2, 2]]
17for k in range(-2, 3)

Inner-middle loop — k runs through {−2, −1, 0, 1, 2}.

18for l in range(-2, 3)

Innermost loop — l runs through {−2, −1, 0, 1, 2}. l varies fastest, so the resulting list is ordered (h major, l minor).

19G_points = hkl @ B

Matrix multiplication. Each row (h, k, l) of `hkl` is dot-multiplied with the columns of B — except the more useful way to read it is row-i of G_points = h·b₁ + k·b₂ + l·b₃, since `hkl @ B` ≡ Σ_j hkl[:, j] · B[j]. In words: 'expand every Miller triple in the reciprocal basis.'

EXECUTION STATE
📚 @ operator = Python's matrix-multiply operator (PEP 465, 2014). For 2-D ndarrays it behaves like np.matmul. Inner dimensions must match: (125, 3) @ (3, 3) → (125, 3).
⬇ hkl (125, 3) = Integer triples. Inner dim = 3 (matches B's row count).
⬇ B (3, 3) = Reciprocal basis, rows = b_i (Å⁻¹). Inner dim = 3 (matches hkl's column count).
⬆ G_points (125, 3) = Reciprocal-space positions in Å⁻¹. Row 62 is (0, 0, 0). Row 63 is (1)·b₃ = (1.0339, 1.0339, -1.0339). Row 0 is -2(b₁ + b₂ + b₃) = (-2.068, -2.068, -2.068).
→ why we need so many points = Voronoi needs every neighbour that could possibly carve a face of the origin's cell. For FCC, the 12 nearest at distance √3·(2π/a) and the 6 next-nearest at distance 2·(2π/a) suffice — but to be safe we include up to ±2 in each direction.
21Comment — running QHull

Voronoi is the magic step. SciPy delegates to QHull, the same C library MATLAB and CGAL use.

22vor = Voronoi(G_points)

Constructs the Voronoi diagram of the 125 points. The diagram contains: every Voronoi vertex (point equidistant from 4+ input points), every region (one polyhedron per input point), and ridge connectivity. Cost is roughly O(N log N) for our small N.

EXECUTION STATE
📚 Voronoi(points) = Constructor. `points` shape (N, d). Returns an object with .vertices (M × d), .regions (list of int lists, one per region), .point_region (length-N int array mapping input idx → region idx), .ridge_points, .ridge_vertices.
⬇ G_points (125, 3) = Our reciprocal lattice within ±2.
⬆ vor.vertices = shape ≈ (200+, 3) — every Voronoi vertex in the 5×5×5 region, in Å⁻¹.
⬆ vor.regions = List of length 126 (125 + 1 sentinel). Each entry is a list of vertex indices forming that region. The unbounded outer regions contain index −1.
⬆ vor.point_region = Length-125 int array. point_region[i] is the index into vor.regions for input point i.
23origin_idx = np.where(np.all(hkl == 0, axis=1))[0][0]

Finds the row of `hkl` that is (0, 0, 0). We need its index because we'll feed it into vor.point_region to ask 'which region is the origin's?' This compound expression is a textbook NumPy idiom — let's unpack each piece.

EXECUTION STATE
📚 hkl == 0 = Element-wise comparison. Broadcasts the scalar 0 against every entry of (125, 3) → returns a (125, 3) bool array.
📚 np.all(..., axis=1) = Reduces along axis 1 (the 3 columns), returning True only if ALL three entries are True. Result shape: (125,).
→ axis=1 explained = axis=0 = 'collapse rows' → shape (3,); axis=1 = 'collapse columns' → shape (125,). For our 'is this row all zero?' query we want axis=1.
📚 np.where(cond) = Returns a tuple of arrays of indices where cond is True. For a 1-D input the tuple has length 1.
[0][0] = First [0]: unwrap the 1-element tuple from np.where. Second [0]: grab the first index from the array. Origin appears exactly once, so this is unambiguous.
⬆ origin_idx = 62 (the (0, 0, 0) row sits at index 62 = 2·25 + 2·5 + 2 of the 5×5×5 ordering)
24region = vor.regions[vor.point_region[origin_idx]]

Two indirections in one line. `vor.point_region[62]` says 'which entry of vor.regions describes input point 62?' — call that REGION_IDX. Then `vor.regions[REGION_IDX]` is the list of vertex indices forming the polyhedron around the origin — i.e., the FIRST BRILLOUIN ZONE.

EXECUTION STATE
vor.point_region[62] = An int — say 87 (depends on QHull's internal ordering). Tells us 'origin's region lives at vor.regions[87]'.
vor.regions[87] = List of 24 ints, each indexing into vor.vertices. Order is QHull-arbitrary; we sort it later if we want a face-by-face traversal.
⬆ region = List of 24 vertex indices. Note 24 = number of W-vertices of the truncated octahedron. ✓
→ bounded check = If origin's region contained -1 we'd know we hadn't included enough neighbours. For FCC ±2, no -1 — the cell is fully bounded.
25bz_vertices = vor.vertices[region]

Fancy indexing: slice rows of `vor.vertices` by the integer list `region`. Result is a (24, 3) array of the 24 W-vertices of the FCC Brillouin zone, in Å⁻¹.

EXECUTION STATE
📚 fancy indexing = When the index is an array of ints, NumPy returns the rows (or elements) at those positions. Shape: (len(region), 3).
⬆ bz_vertices (24, 3) =
First few rows (units Å⁻¹):
[ 1.0339,   0.5170,   0.0000]   ← W₁
[ 1.0339,  -0.5170,   0.0000]   ← W₂
[ 1.0339,   0.0000,   0.5170]   ← W₃
[ 1.0339,   0.0000,  -0.5170]   ← W₄
...
(24 rows total, every permutation of (±1.0339, ±0.5170, 0))
→ in 2π/a units = Each row divided by 2π/a = 1.0339 gives integer-half permutations of (±1, ±½, 0) — exactly what the 3D viewer above renders.
27Comment — sanity-check the geometry

We don't trust QHull blindly. Two cheap checks: (a) vertex count = 24 (truncated octahedron), and (b) reciprocal-cell volume V* = (2π)³/V_FCC.

28print(f"BZ vertex count = {len(bz_vertices)}")

f-string interpolates the length of bz_vertices. The truncated octahedron has 8 hexagonal faces, 6 square faces, and exactly 24 vertices (each shared between 1 square + 2 hexes).

EXECUTION STATE
📚 f-string = Formatted string literal (PEP 498). `f"...{expr}..."` evaluates `expr` and inserts its str() representation. Faster and more readable than .format().
📚 len(arr) = For an ndarray, returns the length of the first axis. For (24, 3) → 24.
⬆ printed = BZ vertex count = 24 ✓
29print(f"V* = {(2*np.pi)**3 / abs(np.linalg.det(A)):.4f} Å⁻³")

Computes and prints the reciprocal-cell volume. The duality V* = (2π)³ / V (Section 3.2's boxed formula) gives an independent number we can match against det(B). If both agree, the entire pipeline is consistent.

EXECUTION STATE
📚 np.linalg.det = Determinant of a square matrix via LU decomposition. For a 3×3 the formula is the scalar triple product of the rows.
📚 abs(...) = Built-in absolute value. We wrap det in abs() because a left-handed basis would give det(A) < 0 and a negative volume — physically nonsense.
📚 :.4f format spec = Inside f-string: `:.4f` means 'fixed-point notation, 4 digits after decimal'. So 4.420987… → '4.4210'.
(2 * np.pi) ** 3 = 248.0502
abs(np.linalg.det(A)) = 56.1019 ų
⬆ printed = V* = 4.4214 Å⁻³ ← exactly det(B); duality verified ✓
30print(f"|b₁| = {np.linalg.norm(B[0]):.4f} Å⁻¹")

Length of the first reciprocal-lattice vector. For FCC primitive (a/2)·(0,1,1)-style basis, every b_i has length √3·(2π/a). For our a = 6.077 Å this is √3 × 1.0339 = 1.7908 Å⁻¹.

EXECUTION STATE
📚 np.linalg.norm(v) = Default is the 2-norm (Euclidean length). For (3,) input: √(v₀² + v₁² + v₂²).
B[0] = [-1.0339, 1.0339, 1.0339]
→ length = √(1.0339² + 1.0339² + 1.0339²) = √(3 × 1.0689) = √3.2067 = 1.7908
⬆ printed = |b₁| = 1.7908 Å⁻¹ ← matches √3 × 2π/a ✓
5 lines without explanation
1import numpy as np
2from scipy.spatial import Voronoi
3
4# 1. FCC primitive lattice vectors (CdSe-like, a = 6.077 Å)
5a = 6.077
6A = (a / 2.0) * np.array([
7    [0.0, 1.0, 1.0],
8    [1.0, 0.0, 1.0],
9    [1.0, 1.0, 0.0],
10])
11
12# 2. Reciprocal lattice — physics convention with 2π
13B = 2 * np.pi * np.linalg.inv(A).T
14
15# 3. Generate reciprocal lattice points within ±2 of origin
16hkl = np.array([(h, k, l) for h in range(-2, 3)
17                          for k in range(-2, 3)
18                          for l in range(-2, 3)])
19G_points = hkl @ B
20
21# 4. Voronoi tessellation — origin's cell IS the first BZ
22vor = Voronoi(G_points)
23origin_idx = np.where(np.all(hkl == 0, axis=1))[0][0]
24region = vor.regions[vor.point_region[origin_idx]]
25bz_vertices = vor.vertices[region]
26
27# 5. Verify the geometry — 24 W-vertices → truncated octahedron
28print(f"BZ vertex count = {len(bz_vertices)}")
29print(f"V*  = {(2 * np.pi) ** 3 / abs(np.linalg.det(A)):.4f} Å⁻³")
30print(f"|b₁| = {np.linalg.norm(B[0]):.4f} Å⁻¹")

Output of the script (verified on Python 3.12 + SciPy 1.13):

📝text
1BZ vertex count = 24
2V*  = 4.4214 Å⁻³
3|b₁| = 1.7908 Å⁻¹

Three numbers, all dictated by the geometry of FCC and the value a=6.077A˚a = 6.077\,\text{Å}. Change aa to the GaAs value 5.653A˚5.653\,\text{Å} and the script prints the GaAs BZ — same shape, different size. Change the FCC template to BCC primitive and you get the rhombic dodecahedron with 14 vertices. The Wigner–Seitz construction is unreasonably general.

Where this script will reappear

The 6-line core (build A, build B, generate G grid, Voronoi, slice, verify) is the same skeleton any structure-aware code uses to tabulate Brillouin-zone geometry — including the open-source toolkit seekpath that VASP wrappers like ASE call to auto-generate high-symmetry paths. By the time you run your first band-structure calculation in Chapter 6, you will not be guessing what those libraries are doing under the hood.


Common Pitfalls

PitfallSymptomFix
Taking the WS cell of the wrong latticeYour FCC band structure shows a cubic BZ.BZ = WS of the RECIPROCAL lattice, not the real one. FCC real → BCC recip → truncated octahedron.
Confusing fractional and Cartesian kBands look fine but the high-symmetry labels are at the wrong x-positions.Check the line just above the KPOINTS coordinates: 'Reciprocal' = fractional in (b₁, b₂, b₃); 'Cartesian' = Å⁻¹.
Sampling the full BZ instead of the IBZCalculations are 24× slower than they need to be on cubic systems.Trust VASP's symmetry detection. ISYM = 2 (default) folds the mesh down to the IBZ. Set ISYM = 0 only if you have a deliberate reason (e.g. broken symmetry from a defect).
Wrong path order on FCC band plotsThe plot labels read in an order that contradicts every textbook.Use the Setyawan–Curtarolo path Γ-X-W-L-Γ-K (or W-L-Γ-X-W-K). Anything else creates needless confusion for a reader.
Including too few neighbours when constructing BZ numericallyscipy.spatial.Voronoi reports an unbounded region (-1 in vor.regions[origin]).Generate at least ±2 in each direction (5×5×5 = 125 points). For low-symmetry monoclinic / triclinic, go to ±3.
Using the conventional cube edge as 'a' in reciprocal formulaeReciprocal vectors look 4× too small for FCC.Always derive b_i from the PRIMITIVE cell. 'a' inside the formula 2π/a refers to the primitive cell magnitude, NOT the conventional cube edge.

Summary

  • The first Brillouin zone is the Wigner–Seitz cell of the reciprocal lattice: the set of points in k-space closer to the origin than to any other reciprocal-lattice point.
  • Its purpose is to remove the redundancy kk+G\mathbf{k} \equiv \mathbf{k} + \mathbf{G} built into Bloch's theorem. Every physical wavevector has exactly one representative in the BZ.
  • For cubic systems: SC → cube; BCC → rhombic dodecahedron (12 faces, 14 vertices); FCC → truncated octahedron (14 faces, 24 vertices).
  • The FCC BZ has six high-symmetry points Γ,X,L,K,W,U\Gamma, X, L, K, W, U at integer-half permutations of (±1,±12,0)2π/a(\pm 1, \pm \tfrac{1}{2}, 0) \cdot 2\pi/a and similar — every CdSe / GaAs / Si band-structure plot is drawn through exactly these.
  • Time-reversal plus the cubic point group OhO_h shrink the BZ to a 1/48 wedge, the irreducible BZ. VASP's IBZKPT file is exactly this set of weighted k-points.
  • Six lines of NumPy + scipy.spatial.Voronoi construct any BZ from the primitive lattice, with two trivially-checked invariants: vertex count (24 for FCC) and reciprocal volume V=(2π)3/VV^* = (2\pi)^3/V.
  • The KPOINTS file you write tomorrow will, internally, be a discretisation of this exact polyhedron. Everything in the rest of the book — band structures, density of states, optical absorption, structure factors — is an integral over it.
Section 3.3 Core Insight
"The first Brillouin zone is the most symmetric fundamental domain of reciprocal space — built by perpendicular bisectors, decorated by the points group theory says must matter, and small enough to compute on."
Coming next: Section 3.4 — High-Symmetry Points and Paths — where we name every Γ,X,L,K,W,U\Gamma, X, L, K, W, U for the BZ shapes we just met, and turn them into the band-structure paths you will plot in Chapter 5.
Loading comments...