Crystal lattices¶
A crystal is a lattice plus a basis: an infinite, periodic
arrangement of points where each point carries one or more atoms.
Every periodic calculation in vibe-qc — PeriodicSystem,
run_rhf_periodic_scf, monkhorst_pack, band-structure plots —
starts from one of the 14 Bravais lattices, decorated with the
basis atoms that turn it into a real material.
This page is the reference for which lattice to pick, how to set it up in vibe-qc, and what the Brillouin zone of each looks like. For the sampling of the BZ (Monkhorst-Pack, IBZ reduction, mesh choice, band paths), see k-point meshes.
Lattice + basis = crystal¶
A Bravais lattice is the set of points
where the lattice vectors \(\mathbf{a}_1, \mathbf{a}_2, \mathbf{a}_3\) define the parallelepiped unit cell. The basis is the list of atoms inside one unit cell:
The full crystal is then \(\{\boldsymbol\tau_j + \mathbf{R}\}\) for every \(\mathbf{R}\) on the lattice and every atom \(j\) in the basis.
Every Bravais lattice has a single point per unit cell. Crystals with 2+ atoms per cell are not multi-atom Bravais lattices — they’re a Bravais lattice with a multi-atom basis. Diamond is FCC + 2-atom basis; NaCl is FCC + 2-atom basis; HCP is hexagonal + 2-atom basis.
In vibe-qc:
import numpy as np
import vibeqc as vq
# 1. Lattice vectors as the rows of a 3×3 array (Å or bohr).
lattice = np.array([
[3.92, 0.00, 0.00],
[0.00, 3.92, 0.00],
[0.00, 0.00, 3.92],
])
# 2. Basis atoms in fractional coordinates of the lattice vectors.
sysp = vq.PeriodicSystem(
dim=3,
lattice=lattice,
unit_cell=[
vq.Atom(11, [0.0, 0.0, 0.0]), # Na at corner
vq.Atom(17, [0.5, 0.5, 0.5]), # Cl at body-centre
],
)
The lattice vectors, the choice of which atom sits at the origin,
and the orientation are all conventions. Different choices are
equivalent if related by a lattice translation; always physically
equivalent if related by a point-group operation.
The 14 Bravais lattices¶
The Bravais classification groups every possible periodic 3D lattice into 14 types, organized under the 7 crystal systems:
# |
Crystal system |
Bravais lattices |
Constraint on \(a, b, c\) |
Constraint on \(\alpha, \beta, \gamma\) |
|---|---|---|---|---|
1 |
Triclinic |
aP |
none |
none |
2 |
Monoclinic |
mP, mC (or mB) |
none |
\(\alpha = \gamma = 90°\), \(\beta \neq 90°\) |
3 |
Orthorhombic |
oP, oC, oI, oF |
none |
\(\alpha = \beta = \gamma = 90°\) |
4 |
Tetragonal |
tP, tI |
\(a = b \neq c\) |
\(\alpha = \beta = \gamma = 90°\) |
5 |
Trigonal (rhombohedral) |
hR |
\(a = b = c\) |
\(\alpha = \beta = \gamma \neq 90°\) |
6 |
Hexagonal |
hP |
\(a = b \neq c\) |
\(\alpha = \beta = 90°\), \(\gamma = 120°\) |
7 |
Cubic |
cP, cI, cF |
\(a = b = c\) |
\(\alpha = \beta = \gamma = 90°\) |
Letter codes: P = primitive, I = body-centered, F = face- centered, C (or A, B) = base-centered (one face), R = rhombohedral primitive.
The 14 unique lattices are:
Cubic: cP cI (BCC) cF (FCC)
Tetragonal: tP tI
Orthorhombic: oP oC oI oF
Hexagonal: hP
Trigonal: hR
Monoclinic: mP mC
Triclinic: aP
Why 14 and not (say) 7 × 4 = 28? Some centrings are equivalent to other Bravais lattices under a different choice of axes — e.g. “face-centered tetragonal” is the same as “body-centered tetragonal” with a 45° in-plane rotation.
Note
The Bravais classification covers only point groups of the lattice. The full classification of crystals (which adds the basis
point-group symmetries) gives the 230 space groups, of which the 14 Bravais lattices are the translational backbone. vibe-qc uses spglib to identify the space group of a
PeriodicSystem—attach_symmetry(sysp)returns the spglib classification + the symmetry operations.
Common materials lattices¶
Most QC use cases reduce to a small list of Bravais lattices with specific basis decorations. Below: the name, primitive lattice vectors (rows of the 3×3 matrix in units of the lattice constant \(a\)), and basis (in fractional coordinates of the primitive lattice).
Cubic family¶
Simple cubic (cP)¶
The textbook lattice — corners of a cube, one atom per cell.
a₁ = (a, 0, 0)
a₂ = (0, a, 0)
a₃ = (0, 0, a)
Basis: (0, 0, 0)
Real materials: α-polonium is the only stable elemental simple-cubic solid at ambient conditions. A useful toy model for periodic SCF testing — large, isotropic, easy to converge.
a = 3.50 # bohr
sysp = vq.PeriodicSystem(
dim=3,
lattice=a * np.eye(3),
unit_cell=[vq.Atom(84, [0.0, 0.0, 0.0])], # Po
)
Body-centered cubic / BCC (cI)¶
Conventional cubic cell with extra atom at the body center. The primitive cell is a rhombohedron one-half the volume of the conventional cubic cell.
Conventional cubic (2-atom basis):
a₁ = (a, 0, 0); a₂ = (0, a, 0); a₃ = (0, 0, a)
Basis: (0, 0, 0), (½, ½, ½)
Primitive (1-atom basis):
a₁ = (a/2)·(-1, 1, 1)
a₂ = (a/2)·( 1, -1, 1)
a₃ = (a/2)·( 1, 1, -1)
Basis: (0, 0, 0)
Real materials: alpha-Fe (3.16 Å), Na, K, Cr, Mo, W, V, Nb, Ta — many transition metals and alkali metals.
Face-centered cubic / FCC (cF)¶
Conventional cubic cell with atoms at the corners + face centers. 4 conventional-cell atoms, primitive cell is one quarter the volume.
Conventional cubic (4-atom basis):
a₁ = (a, 0, 0); a₂ = (0, a, 0); a₃ = (0, 0, a)
Basis: (0, 0, 0), (½, ½, 0), (½, 0, ½), (0, ½, ½)
Primitive (1-atom basis):
a₁ = (a/2)·(0, 1, 1)
a₂ = (a/2)·(1, 0, 1)
a₃ = (a/2)·(1, 1, 0)
Basis: (0, 0, 0)
Real materials: Cu, Ag, Au, Al, Ni, Pt, Pd, gamma-Fe, Pb — most ductile FCC metals plus the noble metals.
Diamond / zincblende (cF + 2-atom basis)¶
FCC primitive lattice + 2-atom basis at \((0,0,0)\) and \((¼,¼,¼)\). Same for diamond (both atoms = C), zincblende (one Zn, one S), GaAs (Ga + As), AlP, BN cubic, etc.
Primitive (FCC):
a₁ = (a/2)·(0, 1, 1)
a₂ = (a/2)·(1, 0, 1)
a₃ = (a/2)·(1, 1, 0)
Basis: (0, 0, 0), (¼, ¼, ¼)
a = 6.74 # bohr (Si)
sysp = vq.PeriodicSystem(
dim=3,
lattice=(a / 2) * np.array([
[0, 1, 1],
[1, 0, 1],
[1, 1, 0],
]),
unit_cell=[
vq.Atom(14, [0.00, 0.00, 0.00]), # Si at primitive-cell origin
vq.Atom(14, [0.25, 0.25, 0.25]), # Si at (¼,¼,¼)
],
)
Real materials: diamond (C, a = 6.74 bohr), Si (a = 10.26 bohr), Ge (a = 10.69 bohr); zincblende ZnS, GaAs, InP, InSb, AlP, AlAs, BN(c).
Rocksalt (cF + 2-atom basis)¶
Same FCC primitive lattice, but the second atom sits at \((½,½,½)\) — i.e. two interpenetrating FCC lattices offset by half the conventional cube diagonal.
Primitive (FCC):
a₁ = (a/2)·(0, 1, 1)
a₂ = (a/2)·(1, 0, 1)
a₃ = (a/2)·(1, 1, 0)
Basis: (0, 0, 0), (½, ½, ½)
Real materials: NaCl (a = 10.65 bohr), KCl, LiF, MgO, CaO — all the
“alkali halide” / “alkaline-earth oxide” prototypes. The Madelung
constant cross-validation in
examples/input-madelung-constants.py
runs on rocksalt + zincblende + CsCl.
CsCl (cP + 2-atom basis)¶
Simple cubic (not BCC!) with one atom at the corner, one at the body center. Distinct from BCC because the two basis atoms are different elements — the center atom isn’t related by translation to the corner atom.
a₁ = (a, 0, 0); a₂ = (0, a, 0); a₃ = (0, 0, a)
Basis: Cs at (0, 0, 0), Cl at (½, ½, ½)
Real materials: CsCl (a = 7.78 bohr), CsBr, NH₄Cl (high-T), TlBr.
Cubic perovskite (cP + 5-atom basis)¶
ABX₃ — one A cation at the corner, one B cation at the center, three X anions on the face centers.
a₁ = (a, 0, 0); a₂ = (0, a, 0); a₃ = (0, 0, a)
Basis:
A at (0, 0, 0) # large cation, e.g. Sr, Ba, Ca
B at (½, ½, ½) # small cation, e.g. Ti, Zr, Mn
X at (½, ½, 0) # anion
X at (½, 0, ½)
X at (0, ½, ½)
Real materials: SrTiO₃, BaTiO₃ (rt cubic), CaTiO₃, KMnF₃ (above the ferro-distortion transition). Ferroelectrics are typically distorted perovskites — the cubic phase is the high-symmetry parent.
Hexagonal family¶
Simple hexagonal / hP¶
a₁ = (a, 0, 0)
a₂ = (-a/2, a√3/2, 0)
a₃ = (0, 0, c)
Basis: (0, 0, 0)
Real materials: rare on its own (graphene is hP 2D); most hexagonal solids have a 2-atom basis (HCP) or 4-atom (graphite).
Hexagonal close-packed / HCP (hP + 2-atom basis)¶
Same lattice vectors as hP.
Basis: (0, 0, 0), (⅓, ⅔, ½)
For ideal HCP: c/a = sqrt(8/3) ≈ 1.633
Real materials: Mg (c/a = 1.624), Zn (c/a = 1.86 — non-ideal), Be, Ti (alpha-phase), Co (alpha), Os, Ru.
Wurtzite (hP + 4-atom basis)¶
Hexagonal binary structure analogous to zincblende. ABAB stacking of close-packed planes (vs ABCABC for zincblende).
Same lattice vectors as hP.
Basis (X = small atom, Y = large atom):
X at (0, 0, 0)
X at (⅓, ⅔, ½)
Y at (0, 0, u) # u ≈ 3/8 ideal
Y at (⅓, ⅔, ½ + u)
Real materials: ZnO, GaN, AlN, CdS, SiC (4H/6H polytypes), BN(h).
Graphene (2D) and graphite (3D)¶
Graphene is a 2D hexagonal lattice with a 2-atom basis (the two
sublattices A and B). vibe-qc handles 2D systems via dim=2 on
PeriodicSystem:
a = 4.65 # bohr (graphene C-C ≈ 1.42 Å → a ≈ 2.46 Å = 4.65 bohr)
sysp = vq.PeriodicSystem(
dim=2,
lattice=np.array([
[a, 0.0, 0.0],
[-a/2, a * np.sqrt(3)/2, 0.0],
[0.0, 0.0, 30.0], # 30 bohr vacuum perpendicular
]),
unit_cell=[
vq.Atom(6, [0.0, 0.0, 0.0]),
vq.Atom(6, [1.0/3, 2.0/3, 0.0]),
],
)
Graphite is graphene stacked along \(c\) with AB Bernal stacking; 4-atom basis.
Tetragonal, orthorhombic, monoclinic, triclinic¶
These cover lower-symmetry structures: distorted perovskites, elemental tin (β-Sn is tI), molecular crystals (most are monoclinic or triclinic), high-Tc superconductors (oC, oI).
vibe-qc accepts arbitrary lattice vectors, so any of these works
out of the box. For symmetry analysis, attach_symmetry(sysp)
runs spglib and returns the space group regardless of which
crystal-system label you’d pick by eye.
Primitive vs conventional cells¶
The primitive cell is the smallest cell that tiles all of space under lattice translations. For BCC + FCC lattices, the primitive cell is not the cubic cell most textbooks draw — it’s a rhombohedron half the volume (BCC) or quarter the volume (FCC).
You’d think you always want the primitive cell — fewer atoms = faster SCF. Two reasons to use the conventional cell instead:
Symmetry. Cubic cells make x, y, z directions explicit. The k-mesh is naturally aligned to the conventional cell’s axes; band paths are conventionally specified in conventional-cell coordinates (Γ, X, M, R, K).
Multi-atom bases. Diamond, NaCl, etc. need 2-atom bases on the FCC primitive cell. Some users find it easier to build the conventional cell with a multi-atom basis (4 + 4 = 8 atoms for diamond) than to set up the primitive cell + 2-atom basis. The energy is identical; the multi-k cost is roughly 4× higher for the same physical system.
vibe-qc respects whichever cell you build. Use attach_symmetry +
spglib to convert between them when needed:
from vibeqc import attach_symmetry, analyze_symmetry
attach_symmetry(sysp)
sg = analyze_symmetry(sysp)
print(f"Space group: {sg.international} (#{sg.number})")
print(f"Hall: {sg.hall}")
print(f"#operations: {len(sg.rotations)}")
# Spglib's primitive cell is sg.primitive_lattice + sg.primitive_basis
The reciprocal lattice and the Brillouin zone¶
For lattice vectors \(\mathbf{a}_1, \mathbf{a}_2, \mathbf{a}_3\), the reciprocal lattice is generated by
with \(V = \mathbf{a}_1 \cdot (\mathbf{a}_2 \times \mathbf{a}_3)\) the unit-cell volume. A general reciprocal-lattice vector is
The first Brillouin zone (BZ) is the Wigner-Seitz cell of the reciprocal lattice — the set of \(\mathbf{k}\) closer to \(\mathbf{G}=0\) than to any other reciprocal-lattice point. Bloch’s theorem says every electronic state in a periodic crystal can be labeled by a \(\mathbf{k}\) in this region.
The BZ shape depends on the Bravais lattice:
Real-space lattice |
BZ shape |
|---|---|
Simple cubic (cP) |
Cube |
BCC (cI) |
Rhombic dodecahedron |
FCC (cF) |
Truncated octahedron |
Hexagonal (hP) |
Hexagonal prism |
Trigonal (hR) |
Distorted hexagonal prism |
vibe-qc doesn’t currently provide a BZ-shape visualization tool; external packages like seekpath or pymatgen do this well.
High-symmetry points¶
These are the special \(\mathbf{k}\)-points where the BZ has extra symmetry. Band structures conventionally trace a path through them. The Setyawan-Curtarolo (2010) convention is what almost everyone uses today:
BZ shape |
High-symmetry points |
|---|---|
Cube (cP) |
Γ, X, M, R |
Rhombic dodecahedron (cI / BCC) |
Γ, H, N, P |
Truncated octahedron (cF / FCC) |
Γ, X, K, L, U, W |
Hexagonal prism (hP) |
Γ, M, K, A, L, H |
The \(\Gamma\) point is always at the BZ center — \(\mathbf{k} = (0,0,0)\) in any units. Other points are at half- or quarter-cell positions in reciprocal space.
For example, on FCC (using conventional-cell reciprocal vectors):
Γ = (0, 0, 0 ) # BZ centre
X = (½, 0, ½ ) # face centre of cube
L = (½, ½, ½ ) # body-diagonal corner of cube
K = (3/8, 3/8, 3/4 ) # midpoint of two adjacent X's
W = (½, ¼, ¾ ) # corner of square face
The standard FCC band path is Γ → X → W → K → Γ → L → U → W → L → K, which traces all of the BZ’s irreducible piece.
Setting up lattices in vibe-qc¶
Tip
For any non-trivial structure, import from a CIF / POSCAR file rather than typing lattice vectors by hand. Vibe-qc reads:
Crystal.from_cif(path)— Crystallographic Information File (the standard format for published structures).read_poscar(path)— VASP 5 POSCAR.Molecule.from_xyz(path)— for molecules in periodic boxes.
The Materials Project (https://materialsproject.org/), Crystallography Open Database (https://crystallography.net/cod/), and ICSD (commercial) all export CIF directly.
For test systems and small examples, building by hand is straightforward:
import numpy as np
import vibeqc as vq
# --- FCC primitive (Cu, a = 3.61 Å) -----------------------------
a = 6.82 # bohr
fcc_primitive = (a / 2) * np.array([
[0, 1, 1],
[1, 0, 1],
[1, 1, 0],
])
cu_fcc = vq.PeriodicSystem(
dim=3, lattice=fcc_primitive,
unit_cell=[vq.Atom(29, [0, 0, 0])],
)
# --- Diamond (Si, a = 5.43 Å) -----------------------------------
a = 10.26 # bohr
si_diamond = vq.PeriodicSystem(
dim=3, lattice=(a / 2) * np.array([
[0, 1, 1], [1, 0, 1], [1, 1, 0],
]),
unit_cell=[
vq.Atom(14, [0.00, 0.00, 0.00]),
vq.Atom(14, [0.25, 0.25, 0.25]),
],
)
# --- Rocksalt (NaCl, a = 5.64 Å) --------------------------------
a = 10.65 # bohr
nacl = vq.PeriodicSystem(
dim=3, lattice=(a / 2) * np.array([
[0, 1, 1], [1, 0, 1], [1, 1, 0],
]),
unit_cell=[
vq.Atom(11, [0.0, 0.0, 0.0]),
vq.Atom(17, [0.5, 0.5, 0.5]),
],
)
# --- Hexagonal close packed (Mg, a = 3.21 Å, c = 5.21 Å) --------
a, c = 6.06, 9.85 # bohr
mg_hcp = vq.PeriodicSystem(
dim=3, lattice=np.array([
[a, 0.0, 0.0],
[-a/2, a * np.sqrt(3) / 2, 0.0],
[0.0, 0.0, c ],
]),
unit_cell=[
vq.Atom(12, [0.0, 0.0, 0.0]),
vq.Atom(12, [1.0/3, 2.0/3, 0.5]),
],
)
What’s next¶
For how vibe-qc samples the BZ of any of these lattices — Monkhorst-Pack meshes, Γ-only vs Γ-centered, IBZ reduction, mesh convergence, k-paths for band structures — see k-point meshes.
For how to run a periodic SCF on one of these lattices — EWALD_3D Coulomb dispatch, periodic Becke partition for DFT, level shifts and smearing for SCF stability — see Ewald summation and SCF convergence.
For band-structure plots along the high-symmetry paths above, see band structure and tutorial 12: band structure of an H-chain.
For symmetry exploitation in periodic SCF (compressing the lattice-matrix set under the space group), see tutorial 25: symmetry-aware periodic storage.
References¶
M. I. Aroyo, ed., International Tables for Crystallography, Vol. A, “Space-Group Symmetry” (8th ed., Wiley, 2024) — the authoritative reference for space groups, Wyckoff positions, and crystallographic conventions.
W. Setyawan and S. Curtarolo, “High-throughput electronic band structure calculations: Challenges and tools,” Comput. Mater. Sci. 49, 299 (2010) — defines the Bravais-lattice-specific k-paths used today.
C. Bradley and A. Cracknell, The Mathematical Theory of Symmetry in Solids (Oxford, 1972) — the textbook reference for space-group representations.
Spglib documentation, https://spglib.readthedocs.io/ — the symmetry-finder vibe-qc uses internally; particularly the “Definition of unit cell” page on conventions.