From the ladder to a spectrum¶
Lecture 4.2 found the vibrational levels of a bond, , evenly spaced by . A molecule absorbs a photon when the photon energy matches the gap between two levels. This lecture asks which gaps light can actually bridge, what the measured lines reveal about bonds, and how real molecules differ from the perfect spring.
Spectroscopists count in wavenumbers¶
Infrared spectra are plotted against the wavenumber , the number of wavelengths per centimeter. It is proportional to the photon’s energy and frequency:
So any energy divided by becomes a wavenumber. For the oscillator, with :
Mind the symbols: the italic is the quantum number, (“nu tilde”) the wavenumber.
What one wavenumber is worth:
| quantity | value | what it tells you |
|---|---|---|
| 1 cm | J eV kJ/mol | the conversion factor |
| 1 eV | 8066 cm | a typical chemical-bond scale is a few eV |
| at 300 K | 208 cm | below this, levels are thermally populated |
| bond vibrations | 200 to 4400 cm | wavelengths 2.3 to 50 μm: the infrared |
Which transitions light can drive¶
Start with a puzzle. N vibrates at 2330 cm and HCl at 2886 cm. Both lie in the middle of the infrared, and both ladders have rungs spaced to match an infrared photon. Yet the air, mostly N, is transparent to that light, while HCl gas absorbs it strongly. Matching the photon to a gap is only the tuning. Something else decides whether the molecule can take the photon at all.
Light pushes on the dipole¶
Light is an oscillating electric field, written here because is an energy. Its wavelength, a few micrometers, is about times the length of the HCl bond. So the field is uniform across the molecule, and it does the one thing a uniform field can do: push positive charges one way and negative charges the other. It therefore couples to the dipole moment , with energy . (Chemistry texts often write the dipole moment as , but here is the reduced mass, and a lowercase would look like a differential.)
The dipole depends on the bond length: , with the stretch. The force the field exerts on the stretch is
The field’s handle on the bond is the slope of the dipole, not the dipole itself. A vibrating bond with is a tiny antenna: its dipole oscillates at the vibration frequency and couples to light at that frequency.
Source
# synced: dipole_antenna
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from IPython.display import HTML
TEAL, CARDINAL, GRAY, PURPLE, ORANGE = "#107895", "#C8102E", "#6c757d", "#6a3d9a", "#e07b00"
from matplotlib.patches import Circle
plt.rcParams.update({"axes.spines.top": False, "axes.spines.right": False})
nf = 32
ts = np.arange(nf) / nf # one loop of the animation = two vibrations
GREEN, BLUE = "#2f9e44", "#1c7ed6"
rows = [(("H", "Cl"), ("white", GREEN), (0.15, 0.29), (1.0, 35.0), True,
"HCl: the dipole swings with the bond", r"$M(t) = M_e + M'x(t)$: the swing couples to light"),
(("N", "N"), (BLUE, BLUE), (0.24, 0.24), (14.0, 14.0), False,
r"N$_2$: symmetric, no dipole at any length", r"$M = 0$ always: light has nothing to push on")]
fig, axes = plt.subplots(2, 2, figsize=(9.6, 4.6), gridspec_kw={"width_ratios": [1, 1.2], "hspace": 0.65, "wspace": 0.12})
tt = np.linspace(0, 1, 300)
parts = []
for (ax, bx), (labels, cols, rads, masses, polar, t1, t2) in zip(axes, rows):
ax.set_xlim(-1.5, 1.5); ax.set_ylim(-0.6, 1.0); ax.set_aspect("equal"); ax.axis("off")
ax.set_title(t1, loc="left", fontsize=12)
(bond,) = ax.plot([], [], color="#adb5bd", lw=6, solid_capstyle="round", zorder=2)
c1 = Circle((0, 0), rads[0], fc=cols[0], ec="k", lw=1, zorder=3); ax.add_patch(c1)
c2 = Circle((0, 0), rads[1], fc=cols[1], ec="k", lw=1, zorder=3); ax.add_patch(c2)
l1 = ax.text(0, 0, labels[0], ha="center", va="center", fontsize=10, weight="bold", zorder=4,
color="k" if cols[0] == "white" else "white")
l2 = ax.text(0, 0, labels[1], ha="center", va="center", fontsize=10, weight="bold", zorder=4, color="white")
(bar,) = ax.plot([], [], color=ORANGE, lw=6, solid_capstyle="butt")
if polar:
ax.text(-0.62, -0.5, r"$\delta+$", ha="center", fontsize=11, color=CARDINAL)
ax.text(0.62, -0.5, r"$\delta-$", ha="center", fontsize=11, color=BLUE)
ax.text(-1.45, 0.72, "dipole", fontsize=11, color=ORANGE, va="center")
else:
ax.text(-1.45, 0.72, r"dipole: $M = 0$", fontsize=11, color=ORANGE, va="center")
bx.axhline(0, color=GRAY, lw=0.8)
if polar:
bx.axhline(1, color=GRAY, lw=1, ls=":")
bx.text(1.0, 1.08, r"$M_e$", ha="right", fontsize=11, color=GRAY)
bx.plot(tt, (1 + 0.45 * np.sin(4 * np.pi * tt)) if polar else 0 * tt, color="#dee2e6", lw=1.8)
(trace,) = bx.plot([], [], color=ORANGE, lw=2.4)
(dot,) = bx.plot([], [], "o", color=ORANGE, ms=7)
bx.set_xlim(0, 1); bx.set_ylim(-0.25, 1.75); bx.set_yticks([]); bx.set_xticks([])
bx.spines["bottom"].set_visible(False)
bx.set_xlabel("time", fontsize=11); bx.set_ylabel(r"dipole $M(t)$", fontsize=11.5)
bx.set_title(t2, loc="left", fontsize=12)
parts.append((masses, polar, bond, c1, c2, l1, l2, bar, trace, dot))
fig.subplots_adjust(left=0.02, right=0.98, top=0.9, bottom=0.08)
def update(i):
s = 0.2 * np.sin(4 * np.pi * ts[i]) # the stretch, exaggerated
for (m1, m2), polar, bond, c1, c2, l1, l2, bar, trace, dot in parts:
p1, p2 = -0.6 - s * m2 / (m1 + m2), 0.6 + s * m1 / (m1 + m2)
bond.set_data([p1, p2], [0, 0])
c1.center, c2.center = (p1, 0), (p2, 0)
l1.set_position((p1, 0)); l2.set_position((p2, 0))
mu = 1 + 2.25 * s if polar else 0.0
bar.set_data([-0.62, -0.62 + 0.9 * mu], [0.72, 0.72])
sel = tt <= ts[i] + 1e-9
trace.set_data(tt[sel], (1 + 0.45 * np.sin(4 * np.pi * tt[sel])) if polar else 0 * tt[sel])
dot.set_data([ts[i]], [mu])
ani = FuncAnimation(fig, update, frames=nf, interval=90, blit=False)
plt.close(fig)
HTML(ani.to_jshtml())Fig. Top: HCl vibrating, with the partial charges on H and on Cl. Its dipole (orange bar) grows and shrinks with the bond length, so oscillates about its equilibrium value ; that oscillation couples to light. Bottom: N has no dipole at any bond length, so there is nothing for the light to push on. The stretch is exaggerated.
A homonuclear diatomic (H, N, O) is symmetric at every bond length, so always and : the field has no handle on the bond. N and O, 99% of the air, are transparent to infrared light. That settles the puzzle. Raman spectra, later in this lecture, still see them: they sense the polarizability, which does change as N stretches.
A heteronuclear diatomic (HCl, CO, NO) always has some , and absorbs.
A stationary state is not an antenna¶
The classical picture says what to look for: a dipole that oscillates. An energy eigenstate cannot supply one. Its density never changes in time (Lecture 3.7), so its average dipole is constant. A molecule that absorbs a photon passes through a superposition of the two levels, and Lecture 3.7 showed that a superposition moves at the Bohr frequencies. Apply that result to the dipole. Take real coefficients and the real oscillator states:
The density has two constant parts and one cross term, whose two phases combine into a cosine ():
Weight it by the dipole and integrate, :
Two readings of the same integral are worth keeping:
The size of the antenna. If , the dipole of the mixed state never swings. Light at has nothing to push on, and it can never move a molecule from to .
A projection. The field enters the energy as , so it acts on a state by multiplying by . Expand the result in oscillator states as in Lecture 3.6: the coefficient of is . The light can feed only the states that this kicked function contains.
The same integral decided which lines an electron in a box can emit in Lecture 3.7, where parity removed the frequency . Here it decides which rungs light can climb.
Each term of the dipole has one job¶
Expand the dipole about :
Sandwich it between two vibrational states:
The permanent dipole gives for , by orthogonality. Multiplying by a constant leaves the state where it was, so a permanent dipole alone cannot drive a vibrational transition.
The slope is the force term of the classical picture. By Lecture 4.2, moves one rung up or down, so this term drives only . From the ground state, is itself, times .
The curvature multiplies , which moves up to two rungs. It allows , but is small, so the line is weak. Together with the anharmonic states later in this lecture, it produces the overtones.
Source
# synced: dipole_kick
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from IPython.display import HTML
TEAL, CARDINAL, GRAY, PURPLE, ORANGE = "#107895", "#C8102E", "#6c757d", "#6a3d9a", "#e07b00"
plt.rcParams.update({"axes.spines.top": False, "axes.spines.right": False})
x = np.linspace(-4.5, 4.5, 1201) # units of 1/sqrt(alpha)
dx = x[1] - x[0]
nv = 5
psi = np.zeros((nv, x.size)) # normalized oscillator states, filled by the recursion
psi[0] = np.pi ** -0.25 * np.exp(-x ** 2 / 2)
psi[1] = np.sqrt(2) * x * psi[0]
for v in range(1, nv - 1):
psi[v + 1] = np.sqrt(2 / (v + 1)) * x * psi[v] - np.sqrt(v / (v + 1)) * psi[v - 1]
cols = [(np.ones_like(x), r"constant term $M_e$", r"$1\cdot\psi_0 = \psi_0$", None,
r"$\langle v'|1|0\rangle$: no transition"),
(x, r"linear term $M'x$", r"$x\,\psi_0 = \frac{1}{\sqrt{2}}\,\psi_1$", (1, r"$\frac{1}{\sqrt{2}}\psi_1$"),
r"$\langle v'|x|0\rangle$: only $0 \to 1$"),
(x ** 2, r"quadratic term $\frac{1}{2}M''x^2$", r"$x^2\psi_0 = \frac{1}{2}\psi_0 + \frac{1}{\sqrt{2}}\,\psi_2$",
(2, r"$\frac{1}{\sqrt{2}}\psi_2$"), r"$\langle v'|x^2|0\rangle$: $0 \to 2$ too")]
fig, axes = plt.subplots(2, 3, figsize=(9.6, 4.1), gridspec_kw={"height_ratios": [1.2, 1], "hspace": 0.78, "wspace": 0.2})
for j, (f, head, ident, part, verdict) in enumerate(cols):
ax, bx = axes[0, j], axes[1, j]
kick = f * psi[0]
comp = psi @ kick * dx # <v'| f |0> for v' = 0..4
ax.axhline(0, color=GRAY, lw=0.8)
ax.plot(x, psi[0], color=GRAY, lw=1.6, ls="--")
ax.plot(x, kick, color=ORANGE, lw=2.6)
if part is not None:
ax.plot(x, comp[part[0]] * psi[part[0]], color=TEAL, lw=2.2, ls=(0, (1.5, 1.8)))
ax.text(1.9, 0.5, part[1], color=TEAL, fontsize=13)
ax.set_xlim(-4, 4); ax.set_ylim(-0.5, 0.8); ax.set_yticks([]); ax.set_xticks([])
ax.spines["left"].set_visible(False); ax.spines["bottom"].set_visible(False)
ax.set_title(head + "\n" + ident, loc="left", fontsize=13, linespacing=1.6)
bx.bar(np.arange(nv), comp, width=0.6, color=[PURPLE] + [ORANGE] * (nv - 1))
for v in range(nv):
if abs(comp[v]) > 0.005:
bx.text(v, comp[v] + 0.05, f"{comp[v]:.2f}", ha="center", va="bottom", fontsize=12,
color=PURPLE if v == 0 else ORANGE)
bx.axhline(0, color=GRAY, lw=0.8)
bx.set_ylim(0, 1.3); bx.set_yticks([]); bx.spines["left"].set_visible(False)
bx.set_xticks(range(nv)); bx.tick_params(labelsize=12)
bx.set_xlabel(r"$v'$", fontsize=13, labelpad=0)
bx.set_title(verdict, loc="left", fontsize=13)
axes[0, 1].text(-1.9, 0.66, r"$\psi_0$", color=GRAY, fontsize=13)
axes[0, 0].text(1.35, 0.52, "after the kick", color=ORANGE, fontsize=12.5)
fig.subplots_adjust(left=0.02, right=0.99, top=0.84, bottom=0.11)
plt.show()
Fig. What each term of the dipole does to the ground state, in units with . Top: multiplying (gray dashed) by 1, or gives the orange function. lies exactly on (teal dotted), and is plus . Bottom: the components of each result. The purple bar () leaves the molecule where it was; the orange bars are transitions.
Slosh or breathe¶
The widget shows the antenna at work. It mixes with a higher level and draws the density, the dipole function , and the swing of . Switch the terms of on and off and watch which lines light up.
sloshes from side to side at . Only a tilted turns the slosh into a swinging dipole: the fundamental needs .
breathes. Its width swings at while its center stays put, so a straight has nothing to grab. Only the curvature feels the width, and the overtone is weak because is small.
moves too, but neither nor reaches three rungs. In a real bond, comes from the anharmonic states and is weaker still.
The quantum frequencies are the classical ones. The fundamental is the bond’s own vibration frequency . The overtone at is the second harmonic that a classical spring would radiate if its dipole had a curvature, since .
One rung at a time¶
The slope term is by far the largest, so keep it alone. Position connects only neighboring levels, unless , so in a harmonic bond light climbs one rung at a time, and every allowed jump absorbs the same wavenumber .
Where the molecules start¶
A spectrum also depends on which levels are occupied. By the Boltzmann factor, the population of relative to is ; for HCl at 300 K that is (Lecture 4.1). Nearly every molecule starts in , so the absorption is the single jump , called the fundamental: one strong line per infrared-active vibration.
Stiffness and mass set the wavenumber¶
The vibrational wavenumber has two knobs, : a stiff bond raises it, heavy atoms lower it. The force constant follows from a measured wavenumber, :
| molecule | (cm) | (u) | (N/m) | infrared |
|---|---|---|---|---|
| H | 4401 | 0.504 | 575 | inactive |
| HF | 4138 | 0.957 | 966 | active |
| HCl | 2991 | 0.980 | 516 | active |
| HBr | 2649 | 0.995 | 411 | active |
| HI | 2309 | 1.000 | 314 | active |
| N | 2359 | 7.00 | 2295 | inactive |
| CO | 2170 | 6.86 | 1902 | active |
| NO | 1904 | 7.47 | 1595 | active |
| O | 1580 | 8.00 | 1177 | inactive |
| Cl | 560 | 17.5 | 323 | inactive |
| I | 215 | 63.5 | 172 | inactive |
Source
# synced: diatomic_ruler
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from IPython.display import HTML
TEAL, CARDINAL, GRAY, PURPLE, ORANGE = "#107895", "#C8102E", "#6c757d", "#6a3d9a", "#e07b00"
plt.rcParams.update({"axes.spines.top": False, "axes.spines.right": False, "axes.spines.left": False})
hyd = [("HF", 4138, 1, 1), ("HCl", 2991, 1, 1), ("HBr", 2649, 1, -1), ("HI", 2309, 1, 1), ("H$_2$", 4401, 0, -1)]
heavy = [("N$_2$", 2359, 0, 1), ("CO", 2170, 1, -1), ("NO", 1904, 1, 1), ("O$_2$", 1580, 0, -1),
("Cl$_2$", 560, 0, 1), ("I$_2$", 215, 0, -1)]
fig, ax = plt.subplots(figsize=(9.6, 3.7))
for y, group in ((1.0, hyd), (0.0, heavy)):
ax.axhline(y, color="#dee2e6", lw=1, zorder=1)
for name, w, act, side in group:
ax.plot([w], [y], "o", ms=12, mfc=TEAL if act else "white", mec=TEAL, mew=2.2, zorder=3)
ax.text(w, y + 0.2 * side, name, ha="center", va="center", fontsize=12.5)
ax.text(4580, 1.0, "one H atom\n" + r"$\mu \approx 1$ u", ha="right", va="center", fontsize=11, color=GRAY)
ax.text(4580, 0.0, "two heavy atoms\n" + r"$\mu$ = 7 to 63 u", ha="right", va="center", fontsize=11, color=GRAY)
ax.annotate("", xy=(2309, 1.42), xytext=(4138, 1.42),
arrowprops=dict(arrowstyle="-|>", color=CARDINAL, lw=1.4, mutation_scale=14))
ax.text(3220, 1.52, "down the halogen group: weaker bond, lower wavenumber", ha="center", fontsize=11, color=CARDINAL)
ax.annotate("", xy=(560, -0.45), xytext=(2359, -0.45),
arrowprops=dict(arrowstyle="-|>", color=PURPLE, lw=1.4, mutation_scale=14))
ax.text(1460, -0.62, r"N$\equiv$N, O=O, Cl$-$Cl: bond order 3, 2, 1", ha="center", fontsize=11, color=PURPLE)
ax.plot([], [], "o", ms=10, mfc=TEAL, mec=TEAL, label="IR active: the dipole changes")
ax.plot([], [], "o", ms=10, mfc="white", mec=TEAL, mew=2, label="IR inactive: no dipole at any length")
ax.legend(loc="upper right", frameon=False, fontsize=11, bbox_to_anchor=(1.0, 1.18), ncol=2)
ax.set_xlim(4600, 0); ax.set_ylim(-0.8, 1.7); ax.set_yticks([])
ax.set_xlabel(r"harmonic wavenumber $\tilde{\nu}$ (cm$^{-1}$)", fontsize=11.5)
fig.subplots_adjust(left=0.17, right=0.99, top=0.88, bottom=0.16)
plt.show()
Fig. Harmonic wavenumbers of common diatomic molecules. Top row: one partner is hydrogen, so u (0.5 u for H). Bottom row: two heavy atoms. Filled circles absorb infrared light; hollow ones have no dipole at any bond length and do not.
Same mass, different stiffness. The hydrogen halides all have u, because the heavy halogen barely moves. Their wavenumbers fall from HF to HI because the bond weakens down the group: drops from 966 to 314 N/m.
Bond order. NN, O=O and ClCl have force constants 2295, 1177 and 323 N/m. More bonding electrons, a stiffer spring.
Mass alone. H is not the stiffest bond in the table, but it is the lightest oscillator ( u), so it vibrates fastest.
Isotopes. The force constant is set by the electrons, which an isotope does not change. Only changes, so .
Real bonds are anharmonic¶
The harmonic levels assume the parabola continues forever. The real bond potential (the Morse curve of Lecture 4.1) is steeper on the compressed side and flattens toward dissociation on the stretched side. Near the bottom the two agree; higher up they do not:
Source
# synced: morse_ladder
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from IPython.display import HTML
TEAL, CARDINAL, GRAY, PURPLE, ORANGE = "#107895", "#C8102E", "#6c757d", "#6a3d9a", "#e07b00"
plt.rcParams.update({"axes.spines.top": False, "axes.spines.right": False})
D, a, re, hw = 4.62, 1.87, 1.275, 0.3708 # HCl: eV, 1/A, A, hbar omega in eV
xe = hw**2 / (4 * D) # Morse anharmonicity (eV)
k = 2 * D * a * a
G = lambda v: hw * (v + 0.5) - xe * (v + 0.5)**2
turn = lambda E: (re - np.log(1 + np.sqrt(E / D)) / a, re - np.log(1 - np.sqrt(E / D)) / a)
r = np.linspace(0.8, 3.8, 700)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9.6, 4.5), sharey=True, gridspec_kw={"wspace": 0.06})
ax1.plot(r, 0.5 * k * (r - re)**2, color=ORANGE, lw=2.4)
for v in range(15):
E = hw * (v + 0.5)
h = np.sqrt(2 * E / k)
ax1.hlines(E, re - h, re + h, color=TEAL if v < 3 else "#adb5bd", lw=1.6 if v < 3 else 1.0)
ax1.set_title("harmonic: equal steps forever,\nthe bond never comes apart", loc="left", fontsize=12, color=ORANGE)
ax2.plot(r, D * (1 - np.exp(-a * (r - re)))**2, color="k", lw=2.4)
vmax = int(hw / (2 * xe) - 0.5) # last level below the dissociation limit
for v in range(vmax + 1):
r1, r2 = turn(G(v))
ax2.hlines(G(v), r1, r2, color=TEAL if v < 3 else "#adb5bd", lw=1.6 if v < 3 else 1.0)
ax2.axhline(D, color=GRAY, lw=1, ls="--")
ax2.text(3.75, D + 0.08, "atoms apart", ha="right", va="bottom", fontsize=11, color=GRAY)
ax2.annotate("", xy=(3.15, D), xytext=(3.15, 0), arrowprops=dict(arrowstyle="<->", color=CARDINAL, lw=1.4, shrinkA=0, shrinkB=0))
ax2.text(3.2, D / 2, r"$D_e$", fontsize=13, color=CARDINAL, va="center")
ax2.annotate("", xy=(2.75, D), xytext=(2.75, G(0)), arrowprops=dict(arrowstyle="<->", color=PURPLE, lw=1.4, shrinkA=0, shrinkB=0))
ax2.text(2.8, D / 2 + 0.3, r"$D_0$", fontsize=13, color=PURPLE, va="center")
ax2.set_title("Morse: the steps shrink, a last level,\nthen dissociation", loc="left", fontsize=12)
for ax in (ax1, ax2):
ax.set_xlim(0.8, 3.8); ax.set_ylim(0, 5.3)
ax.set_xlabel(r"bond length $R$ ($\AA$)", fontsize=11.5)
ax1.set_ylabel("energy (eV)", fontsize=11.5)
fig.subplots_adjust(left=0.07, right=0.99, top=0.86, bottom=0.13)
plt.show()
Fig. Vibrational levels of HCl in the harmonic approximation (left) and in a Morse potential fitted to HCl (right); the lowest three levels are teal. The harmonic steps are equal and never end. The Morse steps shrink as the energy rises, and above the last level the atoms come apart. is the depth of the well from its bottom, the energy needed to dissociate from .
The levels of the Morse potential can be found exactly. The harmonic formula gains one correction term:
is the harmonic wavenumber, set by the curvature at the bottom of the well. The anharmonicity constant is small, typically 0.005 to 0.02.
The steps shrink. The gap above level is
It falls by per rung and reaches zero near . A Morse bond has a finite number of bound levels; above them, the atoms are free.
Dissociation energy. Breaking the bond from costs ; the zero-point energy is part of every bond energy (Lecture 4.2).
Overtones and hot bands¶
In an anharmonic well the selection rule is no longer strict. The states are not exactly harmonic, so connects levels further apart, and the dipole has its own curvature . Jumps , , ... become weakly allowed: the overtones, each tens of times weaker than the one before.
Source
# synced: hcl_overtones
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from IPython.display import HTML
TEAL, CARDINAL, GRAY, PURPLE, ORANGE = "#107895", "#C8102E", "#6c757d", "#6a3d9a", "#e07b00"
plt.rcParams.update({"axes.spines.top": False, "axes.spines.right": False})
we, wexe = 2990.0, 52.0 # cm^-1, from the fundamental and first overtone
G = lambda v: we * (v + 0.5) - wexe * (v + 0.5)**2
D, a, re = 37260.0, 1.87, 1.275 # Morse well in cm^-1 for the drawing
turn = lambda E: (re - np.log(1 + np.sqrt(E / D)) / a, re - np.log(1 - np.sqrt(E / D)) / a)
lines = [(1, TEAL, "fundamental"), (2, ORANGE, "first overtone"), (3, PURPLE, "second overtone")]
fig, (ax, bx) = plt.subplots(1, 2, figsize=(9.6, 4.4), gridspec_kw={"width_ratios": [1, 1.25], "wspace": 0.18})
r = np.linspace(0.95, 2.1, 400)
ax.plot(r, D * (1 - np.exp(-a * (r - re)))**2, color="k", lw=2.2)
for v in range(4):
r1, r2 = turn(G(v))
ax.hlines(G(v), r1, r2, color=GRAY, lw=1.4)
ax.text(r2 + 0.03, G(v), rf"$v = {v}$", va="center", fontsize=11)
for j, (v, col, name) in enumerate(lines):
x = 1.17 + 0.09 * j
ax.annotate("", xy=(x, G(v)), xytext=(x, G(0)),
arrowprops=dict(arrowstyle="-|>", color=col, lw=2.2, mutation_scale=15, shrinkA=0, shrinkB=0))
ax.set_xlim(0.95, 2.1); ax.set_ylim(0, 11500); ax.set_xticks([])
ax.set_ylabel(r"energy (cm$^{-1}$)", fontsize=11.5)
ax.set_title("absorption from $v = 0$", loc="left", fontsize=12)
inten = [1.0, 2.5e-2, 6e-4] # schematic: each overtone tens of times weaker
for (v, col, name), I in zip(lines, inten):
w = G(v) - G(0)
bx.vlines(w, 1e-4, I, color=col, lw=3.2)
bx.text(w, I * 1.6, f"{name}\n{w:.0f} cm$^{{-1}}$", ha="center", va="bottom", fontsize=11, color=col)
bx.set_yscale("log"); bx.set_ylim(1e-4, 20); bx.set_yticks([]); bx.minorticks_off()
bx.set_xlim(2000, 9400)
bx.set_xlabel(r"photon wavenumber (cm$^{-1}$)", fontsize=11.5)
bx.set_ylabel("absorption (log scale)", fontsize=11.5)
bx.set_title(r"not equally spaced: $2 \times 2886 = 5772$, not 5668", loc="left", fontsize=12)
fig.subplots_adjust(left=0.08, right=0.99, top=0.9, bottom=0.13)
plt.show()
Fig. Left: absorption from of HCl to (fundamental), (first overtone) and (second overtone), drawn on the anharmonic ladder. Right: the lines they produce, on a logarithmic intensity scale (schematic). Because the steps shrink, the first overtone sits below twice the fundamental.
Hot bands start from . In HCl the jump absorbs at cm, slightly below the fundamental. At room temperature only one molecule in a million is in , so the hot band is faint; it grows as the gas is heated (Problem 4).
Polyatomic molecules: one oscillator per normal mode¶
A molecule with atoms has normal modes ( if linear), Lecture 4.1. Near equilibrium the vibrational Hamiltonian is a sum of independent harmonic oscillators, one per mode, so by the separable-Hamiltonian rule of Lecture 4.1 the energies add. Each mode has its own wavenumber, and its own selection rule:
Source
# synced: co2_modes
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from IPython.display import HTML
TEAL, CARDINAL, GRAY, PURPLE, ORANGE = "#107895", "#C8102E", "#6c757d", "#6a3d9a", "#e07b00"
from matplotlib.patches import Circle
nf = 36
ts = np.arange(nf) / nf
re, d = 1.0, 0.13 # bond (drawing units) and the oxygen amplitude
f = 2 * 16.0 / 12.0 # carbon moves this much more, to keep the center of mass
# displacements (x, y) of O1, C, O2 per unit amplitude; charges O -1, C +2
modes = [("symmetric stretch", 1388, 2, [(-1, 0), (0, 0), (1, 0)], False),
("antisymmetric stretch", 2349, 3, [(1, 0), (-f, 0), (1, 0)], True),
("bend (two of them)", 667, 1, [(0, -1), (0, f), (0, -1)], True)]
q = np.array([-1.0, 2.0, -1.0])
x0 = np.array([[-re, 0.0], [0.0, 0.0], [re, 0.0]])
fig, axes = plt.subplots(1, 3, figsize=(9.6, 3.6))
parts = []
for ax, (name, wn, cyc, disp, active) in zip(axes, modes):
ax.set_xlim(-1.6, 1.6); ax.set_ylim(-1.0, 1.2); ax.set_aspect("equal"); ax.axis("off")
ax.set_title(f"{name}\n{wn} cm$^{{-1}}$", fontsize=12.5, color=TEAL)
ax.text(0, -0.98, "dipole swings: IR active" if active else "dipole stays 0: IR inactive",
ha="center", va="bottom", fontsize=11.5, color=ORANGE if active else GRAY, weight="bold")
(b1,) = ax.plot([], [], color="#adb5bd", lw=5, zorder=2, solid_capstyle="round")
(b2,) = ax.plot([], [], color="#adb5bd", lw=5, zorder=2, solid_capstyle="round")
atoms = [Circle((0, 0), r, fc=c, ec="k", lw=1, zorder=3) for r, c in ((0.25, CARDINAL), (0.22, "#495057"), (0.25, CARDINAL))]
for c in atoms:
ax.add_patch(c)
arr = ax.annotate("", xy=(0, 0.62), xytext=(0, 0.62),
arrowprops=dict(arrowstyle="-|>", color=ORANGE, lw=2.6, mutation_scale=16, shrinkA=0, shrinkB=0))
parts.append((np.array(disp, float), cyc, b1, b2, atoms, arr))
fig.subplots_adjust(left=0.01, right=0.99, top=0.84, bottom=0.02, wspace=0.05)
def update(i):
for disp, cyc, b1, b2, atoms, arr in parts:
pos = x0 + d * np.sin(2 * np.pi * cyc * ts[i]) * disp
for c, p in zip(atoms, pos):
c.center = tuple(p)
b1.set_data(pos[:2, 0], pos[:2, 1]); b2.set_data(pos[1:, 0], pos[1:, 1])
mu = (q[:, None] * pos).sum(axis=0) * 0.5 # the dipole, scaled for the drawing
base = np.array([0.0, 0.62]) # drawn from a point above the carbon
arr.xy = tuple(base + mu); arr.set_position(tuple(base))
ani = FuncAnimation(fig, update, frames=nf, interval=80, blit=False)
plt.close(fig)
HTML(ani.to_jshtml())Fig. The normal modes of CO, with the measured wavenumbers. The orange arrow is the molecule’s dipole moment, computed from partial charges on C and on each O. In the symmetric stretch the two halves cancel at every instant, so the dipole stays zero. In the antisymmetric stretch and the bend it swings, along the axis and across it. The bend can happen in two perpendicular planes, so there are two bends with the same wavenumber.
CO (, linear) has modes: the symmetric stretch at 1388 cm (silent in the infrared), the antisymmetric stretch at 2349 cm, and two bends at 667 cm.
Water is bent and less symmetric, and all three of its modes absorb (1595, 3657 and 3756 cm, Lecture 4.1).
Modes that are silent in the infrared, such as the symmetric stretch of CO or the stretches of N and O, show up in Raman spectroscopy, which senses how the vibration changes the molecule’s polarizability rather than its dipole.
Reading an infrared spectrum¶
In a larger molecule, a bond such as C=O or OH vibrates almost like an isolated diatomic spring. Its wavenumber depends mostly on the two atoms and the bond order, and hardly on the rest of the molecule. These group frequencies let a chemist read functional groups off a spectrum:
Source
# synced: group_frequencies
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from IPython.display import HTML
TEAL, CARDINAL, GRAY, PURPLE, ORANGE = "#107895", "#C8102E", "#6c757d", "#6a3d9a", "#e07b00"
plt.rcParams.update({"axes.spines.top": False, "axes.spines.right": False, "axes.spines.left": False})
bands = [("O-H", 3200, 3600, TEAL), ("N-H", 3300, 3500, TEAL), ("C-H", 2850, 3100, TEAL),
(r"C$\equiv$N", 2210, 2260, PURPLE), (r"C$\equiv$C", 2100, 2260, PURPLE),
("C=O", 1650, 1780, ORANGE), ("C=C", 1620, 1680, ORANGE), ("C-O", 1000, 1300, GRAY)]
groups = [(0, 2, "stretches of a bond to H:\nthe light H atom", TEAL), (3, 4, "triple bonds:\nthe stiffest", PURPLE),
(5, 6, "double bonds", ORANGE), (7, 7, "single bonds", GRAY)]
fig, ax = plt.subplots(figsize=(9.6, 4.2))
ax.axvspan(1500, 400, color=GRAY, alpha=0.1, lw=0)
ax.text(950, -0.75, "fingerprint region:\nsingle bonds and bends,\nunique to each molecule", ha="center", va="top",
fontsize=10.5, color=GRAY)
n = len(bands)
for j, (name, lo, hi, col) in enumerate(bands):
y = n - 1 - j
ax.barh(y, hi - lo, left=lo, height=0.62, color=col, alpha=0.85)
ax.text(lo - 40, y, name, va="center", ha="left", fontsize=12, color="k")
for j0, j1, txt, col in groups:
ax.text(4520, n - 1 - (j0 + j1) / 2, txt, ha="left", va="center", fontsize=10.5, color=col)
ax.set_xlim(4550, 400); ax.set_ylim(-2.9, n - 0.4); ax.set_yticks([])
ax.set_xticks([4000, 3500, 3000, 2500, 2000, 1500, 1000, 500])
ax.set_xlabel(r"wavenumber (cm$^{-1}$)", fontsize=11.5)
fig.subplots_adjust(left=0.02, right=0.99, top=0.97, bottom=0.13)
plt.show()
Fig. Typical wavenumber ranges of common bond stretches. Below about 1500 cm (gray), single-bond stretches and bends overlap into a pattern unique to each molecule, its fingerprint.
Bonds to hydrogen absorb highest, 2850 to 3600 cm, because the light hydrogen atom makes u. Their force constants are not unusual (Problem 5).
Triple bonds come next (2100 to 2260 cm), then double bonds (1620 to 1780 cm), then single bonds below 1300 cm: the bond order sets .
The same ideas read biological spectra. The backbone of a protein absorbs near 1650 cm in its amide I band, mostly the C=O stretch of each amide group, and the exact wavenumber shifts with the local structure (helix or sheet):

The amide I vibration of a protein backbone, mostly the C=O stretch of each amide group (left), probed by two-dimensional infrared spectroscopy (right).
Normal mode analysis: finding the modes¶
The normal modes of water in Lecture 4.1 came from a computer. Here is how they are found, in five steps. The tools are the Taylor expansion of Lecture 4.1 and the matrices of Lecture 3.5.
Step 1: the potential near the minimum, in every coordinate at once¶
For one bond, Lecture 4.1 expanded the potential about its minimum and kept the first term that survives:
A molecule has many displacements , one per atomic coordinate. The Taylor expansion works the same way with all of them at once:
Measure from the minimum, where every slope vanishes. The first term that survives is again the quadratic one:
The matrix of second derivatives is the Hessian: a force constant for every pair of coordinates. A diagonal entry is the stiffness against moving coordinate alone. An off-diagonal entry says how moving coordinate changes the force on coordinate .
Step 2: Newton’s equations are coupled¶
The force on coordinate is minus the slope of the potential, . (For CO: , which is minus the first row of times .) So
Because of the off-diagonal entries, the equation for each atom contains the positions of its neighbors: the atoms are coupled. Pull one oxygen of CO and let go, and the motion of each atom is a tangle:
Source
# synced: co2_decoupling
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from IPython.display import HTML
TEAL, CARDINAL, GRAY, PURPLE, ORANGE = "#107895", "#C8102E", "#6c757d", "#6a3d9a", "#e07b00"
plt.rcParams.update({"axes.spines.top": False, "axes.spines.right": False})
mO, mC, k = 16.0, 12.0, 1.0
m = np.array([mO, mC, mO])
K = k * np.array([[1, -1, 0], [-1, 2, -1], [0, -1, 1]])
w2, u = np.linalg.eigh(K / np.sqrt(np.outer(m, m)))
w = np.sqrt(np.clip(w2, 0, None))
u = u * np.sign(u[2]) # right oxygen moves +1 in every mode
x0 = np.array([-0.3, 0.0, 0.0]) # pull the left oxygen outward and let go
q0 = np.sqrt(m) * x0
Q0 = u.T @ q0
Q0[0] = 0.0 # drop the translation: the center of mass stays put
t = np.linspace(0, 4, 800) # time in periods of the symmetric stretch
ph = 2 * np.pi * t * w[None, 1:].T / w[1] # phases of the two vibrations
Q = Q0[1:, None] * np.cos(ph) # each normal coordinate: one cosine
x = (u[:, 1:] @ Q) / np.sqrt(m)[:, None] # back to atom displacements
fig, (ax, bx) = plt.subplots(2, 1, figsize=(9.6, 4.8), sharex=True, gridspec_kw={"hspace": 0.45})
for xi, lab, col, ls in ((x[0], "left O", CARDINAL, "-"), (x[1], "C", "#495057", "-"), (x[2], "right O", CARDINAL, "--")):
ax.plot(t, xi, color=col, lw=2, ls=ls, label=lab)
ax.set_title("atom by atom: every displacement mixes two frequencies, and energy flows between the atoms",
loc="left", fontsize=12)
ax.legend(loc="upper right", frameon=False, fontsize=11, ncol=3, bbox_to_anchor=(1.0, 1.02))
ax.set_ylabel("displacement", fontsize=11.5)
bx.plot(t, Q[0], color=TEAL, lw=2.4, label=r"$Q_{\mathrm{sym}}$: symmetric stretch")
bx.plot(t, Q[1], color=ORANGE, lw=2.4, label=r"$Q_{\mathrm{anti}}$: antisymmetric stretch")
bx.set_title(r"mode by mode, $Q_k = \mathbf{u}_k\cdot\mathbf{q}$: each is a single cosine, $\ddot{Q}_k = -\omega_k^2 Q_k$",
loc="left", fontsize=12)
bx.legend(loc="upper right", frameon=False, fontsize=11, ncol=2, bbox_to_anchor=(1.0, 1.02))
bx.set_ylabel("normal coordinate", fontsize=11.5)
bx.set_xlabel("time (periods of the symmetric stretch)", fontsize=11.5)
for a in (ax, bx):
a.axhline(0, color=GRAY, lw=0.8); a.set_yticks([]); a.set_xlim(0, 4)
lo, hi = a.get_ylim(); a.set_ylim(lo, hi + 0.35 * (hi - lo))
fig.subplots_adjust(left=0.06, right=0.99, top=0.93, bottom=0.11)
plt.show()
Fig. CO after its left oxygen is pulled outward and released (one spring per bond, center of mass fixed). Top: the displacement of each atom mixes two frequencies, and energy passes back and forth between the atoms. Bottom: the same motion written in the normal coordinates of Step 4 is two independent cosines, one per mode.
Step 3: weight by the masses, and look for a mode¶
The masses differ from atom to atom. In the coordinates every atom behaves as if it had unit mass:
A normal mode is all the atoms moving in step. Look for a motion in which every coordinate oscillates at the same frequency, keeping fixed proportions :
This is a guess, an ansatz, like the Gaussian tried for the oscillator ground state in Lecture 4.2: substitute it into Newton’s equation and see which and make it work. Step 4 justifies it, by showing that every motion is a sum of such solutions. Two time derivatives give . Newton’s equation gives . The two agree at every instant only if
Finding the normal modes means finding the eigenvectors of (the shapes of the motion) and its eigenvalues (the squared frequencies). CO’s matrix has three: a translation with , the symmetric stretch and the antisymmetric stretch (worked out below). is a real symmetric matrix, , like the Hermitian matrices of Operators 2, so its eigenvalues are real and its eigenvectors are perpendicular:
Step 4: project onto the modes, and they come apart¶
Step 3 found special motions, each at a single frequency. Now show that every motion is made of them, and that they never disturb one another.
The eigenvectors are axes. With coordinates there are perpendicular unit eigenvectors, a full set of axes for . Any configuration is a combination of them,
The normal coordinate is the component of along the axis . Three symbols carry the whole argument, so keep them apart:
| symbol | what it is | changes in time? | CO example |
|---|---|---|---|
| the configuration: every mass-weighted displacement at time , one entry per coordinate | yes | ||
| normal mode : a fixed unit vector, the shape of the motion, an eigenvector of | no | ||
| the normal coordinate: one number, how much of mode is present now, | yes |
The configuration is (how much) times (shape), summed over the modes. It has the structure of a state written in a basis, (Lecture 3.6): plays , the fixed play the basis functions , and the play the coefficients , found by the same kind of projection. The difference is that the oscillate as cosines, where the only turn their phases. In the guess of Step 3, a single and every other is zero.
Project Newton’s equation onto one axis. Take the dot product of with , one step at a time:
The third line moves from one side of the dot product to the other. That is allowed for a symmetric matrix: .
Read the result. Only appears on the right: no other normal coordinate. Each one obeys its own spring equation, , whatever the others are doing. The modes are independent, as in the bottom panel of the figure above.
The motion of the atoms is a sum of modes, , just as a state is a sum of eigenfunctions. Each oscillates at its own , so the atoms (top panel) mix the frequencies.
Step 5: the Hamiltonian becomes a sum¶
The same change of coordinates splits the energy. In the mass-weighted coordinates the kinetic energy is and the potential energy is , so
Now insert . Every product of two different eigenvectors is zero, , so the double sums collapse to single sums:
In atomic coordinates the potential energy has cross terms, such as in CO, which pass energy from atom to atom. In normal coordinates there are none: each mode is a separate oscillator with its own energy, and that energy never leaks into another mode.
Quantum mechanically, is a sum of harmonic oscillators, one per mode. By the separable rule of Lecture 4.1 the wavefunctions multiply and the energies add, : the box at the start of this section.
The widget runs the CO stretches live. Choose how much of each mode the molecule holds, or pull one oxygen, and compare the two ways of splitting the same energy:
Atom by atom (left), the kinetic energy of each atom and the energy of each spring rise and fall as energy passes between them.
Mode by mode (right), each mode trades energy between its own kinetic and potential parts, but its total stays fixed. Only with a single mode does the atom picture repeat itself; with both, it never quite does.
A quantum chemistry “frequency calculation” does exactly this, with computed from the electronic energy.
numpy finds the same three modes from the mass-weighted matrix:
import numpy as np
mO, mC, k = 16.0, 12.0, 1.0 # masses in u; k sets the unit of omega^2
K = k * np.array([[1, -1, 0], [-1, 2, -1], [0, -1, 1]]) # force constants of O-C-O
m = np.array([mO, mC, mO])
F = K / np.sqrt(np.outer(m, m)) # the mass-weighted matrix
w2, u = np.linalg.eigh(F) # eigenvalues omega^2, eigenvectors u
print("omega^2 in units of k/mO:", np.round(w2 * mO / k, 3) + 0)
print("antisymmetric / symmetric frequency:", round(np.sqrt(w2[2] / w2[1]), 3))
x = u[:, 2] / np.sqrt(m) # displacements of the antisymmetric mode
print("antisymmetric mode, O C O:", np.round(x / x[0], 3))omega^2 in units of k/mO: [0. 1. 3.667]
antisymmetric / symmetric frequency: 1.915
antisymmetric mode, O C O: [ 1. -2.667 1. ]
The bends need the full coordinates of each atom and a spring for the bond angle. The water modes of Lecture 4.1 were found this way: three atoms in a plane give a matrix, with three zero eigenvalues (two shifts and one turn in the plane) and three vibrations.
Problems¶
Problem 1: Bond order from wavenumbers¶
The CO stretch of methanol absorbs near 1033 cm, the C=O stretch of formaldehyde near 1746 cm, and the CO stretch of carbon monoxide at 2143 cm. Treat each as a CO diatomic spring and find the ratio of the three force constants. How close is it to 1 : 2 : 3?
Solution
The atoms are the same in all three, so is the same and :
The force constant grows with bond order, but faster than 1 : 2 : 3. A triple bond is shorter as well as more strongly bonded, and in a polyatomic molecule the “CO stretch” also mixes with neighboring motions, so the diatomic picture is only approximate.
Problem 2: Which molecules absorb?¶
Which of these have an infrared-active fundamental: HBr, O, NO, H? For the linear molecule OCS (O=C=S), which of its four modes absorb? Compare with CO and explain the difference.
Problem 3: How many bound levels?¶
Using cm and cm for HCl, find the highest bound vibrational level and the well depth that this Morse potential implies. Compare with the actual eV.
Solution
rises as long as the gap is positive. has its maximum where :
so the levels to 28 are bound, 29 in all. The top of the ladder is the Morse well depth:
The real well is 4.62 eV deep. A two-parameter Morse curve fitted to the lowest levels overestimates the depth by 15%: the real potential flattens out sooner than the Morse form assumes.
Problem 4: Hot bands as a thermometer¶
Find the wavenumber of the HCl hot band . Estimate its intensity relative to the fundamental at 300 K and at 1000 K, assuming both transitions are equally strong per molecule. Why could the ratio serve as a thermometer?
Problem 5: Why bonds to hydrogen absorb high¶
A CH stretch absorbs near 2900 cm and a CC stretch near 1000 cm. Compute both force constants, treating each bond as a diatomic spring. Is the CH bond much stiffer?
Solution
Problem 6: Nitrous oxide¶
NO is linear, N=N=O. How many normal modes does it have? Its stretches absorb near 1285 and 2224 cm and its bend near 589 cm, and all of them are infrared active. Why is the “symmetric” stretch active here when the one in CO is not?
Problem 7: A Birge-Sponer estimate¶
For an anharmonic bond the gap falls linearly with . Using the HCl constants of the overtone example, sketch against , find where it reaches zero, and use the area under the line (the sum of all the gaps) to estimate . Compare with the measured 4.43 eV.
Problem 8: One line, four units¶
Carbon monoxide absorbs at 2143 cm. Give the photon’s wavelength, frequency and energy (in eV and kJ/mol), and the fraction of CO molecules in at 300 K.
Solution
Wavelength: μm.
Frequency: Hz.
Energy: J eV kJ/mol.
Population: relative to , . Fewer than one CO molecule in 20 000 vibrates at room temperature.