Linear algebra is the backbone of quantum mechanics. States are vectors, observables are matrices, and measurement outcomes are eigenvalues. Everything that follows in the course is, at bottom, an eigenvalue problem in a vector space. This page builds that vocabulary from the 2D picture up.
Vectors¶
What is a vector?¶
A vector is an ordered collection of numbers describing the state or configuration of a system:
, a 2D vector,
and , unit vectors along the axes,
, a 3D vector,
, a 4D vector with complex components,
, an infinite-dimensional vector (a sequence).
Vectors may live in real or complex vector spaces depending on their components. In quantum mechanics the components are generally complex.
The plot shows a vector built as , decomposed along the two basis directions.
Source
import numpy as np
import matplotlib.pyplot as plt
e1 = np.array([1, 0])
e2 = np.array([0, 1])
v = 2 * e1 + 3 * e2
fig, ax = plt.subplots(figsize=(6, 5))
ax.set_xlim(-0.5, 3.5)
ax.set_ylim(-0.5, 3.5)
ax.set_aspect('equal')
ax.arrow(0, 0, *e1, head_width=0.1, color='gray', length_includes_head=True)
ax.arrow(0, 0, *e2, head_width=0.1, color='gray', length_includes_head=True)
ax.text(1.05, -0.25, r'$e_1$', fontsize=13)
ax.text(-0.28, 1.05, r'$e_2$', fontsize=13)
ax.arrow(0, 0, *v, head_width=0.15, color='#2e4057', length_includes_head=True)
ax.plot([0, v[0]], [v[1], v[1]], '--', color='gray')
ax.plot([v[0], v[0]], [0, v[1]], '--', color='gray')
ax.text(v[0] + 0.1, v[1] + 0.1, r'$\mathbf{v} = 2e_1 + 3e_2$', color='#2e4057', fontsize=13)
ax.axhline(0, color='black', lw=0.8)
ax.axvline(0, color='black', lw=0.8)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_title('Fig.1 A vector as a linear combination of basis vectors')
plt.tight_layout()
plt.show()
Inner products, norms, and orthogonality¶
The next natural operation multiplies two vectors into a number that measures how much they share a direction. Throughout this section, is orange and is blue, in every equation and every figure.
A worked example, with every number wearing its vector’s color. For and :
The two forms together are a machine for extracting angles from components. And the special case earns its own name:
Vectors that are both orthogonal and normalized are orthonormal, compactly written with the Kronecker delta:
The norm is the square root of a vector’s inner product with itself, giving its length:
A vector with is normalized.
Projection: the most quantum operation in this appendix¶
The answer defines the orthogonal projection. Choose so that the leftover error vector is orthogonal to ; any other choice leaves a longer error (Pythagoras). That choice gives:
Source
a_v = np.array([2.0, 3.0])
b_v = np.array([4.0, 1.0])
p_v = (a_v @ b_v) / (b_v @ b_v) * b_v
fig_pr, ax_pr = plt.subplots(figsize=(6.4, 4.6))
s = np.linspace(-0.5, 1.35, 2)
ax_pr.plot(s * b_v[0], s * b_v[1], color="0.85", lw=1.2, zorder=0)
for vec, color, label, dx, dy in [
(a_v, "orange", r"$\vec a$", -0.25, 0.15),
(b_v, "#3d81f6", r"$\vec b$", 0.1, -0.3),
(p_v, "#004d40", r"$\vec p$", 0.05, 0.22),
]:
ax_pr.annotate("", xy=vec, xytext=(0, 0),
arrowprops=dict(arrowstyle="->", color=color, lw=2.6))
ax_pr.text(vec[0] + dx, vec[1] + dy, label, color=color, fontsize=14)
ax_pr.plot([p_v[0], a_v[0]], [p_v[1], a_v[1]], "--", color="#d81b60", lw=2.2)
ax_pr.text((p_v[0] + a_v[0]) / 2 + 0.12, (p_v[1] + a_v[1]) / 2, r"$\vec e$",
color="#d81b60", fontsize=14)
u_hat = b_v / np.linalg.norm(b_v)
n_hat = np.array([-u_hat[1], u_hat[0]])
sq = 0.16
corner = p_v
ax_pr.plot([corner[0] - sq * u_hat[0], corner[0] - sq * u_hat[0] + sq * n_hat[0]],
[corner[1] - sq * u_hat[1], corner[1] - sq * u_hat[1] + sq * n_hat[1]],
color="0.4", lw=1)
ax_pr.plot([corner[0] - sq * u_hat[0] + sq * n_hat[0], corner[0] + sq * n_hat[0]],
[corner[1] - sq * u_hat[1] + sq * n_hat[1], corner[1] + sq * n_hat[1]],
color="0.4", lw=1)
ax_pr.set_xlim(-0.8, 5.2)
ax_pr.set_ylim(-0.8, 3.6)
ax_pr.set_aspect("equal")
ax_pr.grid(True, ls=":", alpha=0.5)
ax_pr.set_title("the projection p is the shadow of a along b; the error e is orthogonal", fontsize=10)
plt.show()
The picture contains a decomposition. Walking along and then along lands exactly on :
Now the quantum payoff. For an orthonormal basis the projection formula collapses to , and expanding a vector in a basis is nothing but projecting it onto each basis direction in turn:
In quantum mechanics the state is the vector, the eigenstates of a measurement are the orthonormal basis, and the numbers become the probabilities of each outcome (the Born rule). Every quantum measurement is, at its mathematical heart, the orthogonal projection drawn above.
Solution
, so and the normalized vector is . Forgetting the complex conjugate gives : a nonzero vector with zero length, which is the classic sign you dropped the star.
Matrices¶
Linear systems as matrix equations¶
A system of linear equations is a matrix acting on a vector. The system
packs into a single matrix-vector product:
Matrix times vector is the dot product of each row of with :
If is invertible, the solution is .
import numpy as np
A = np.array([[2, 3],
[1, -2]])
b = np.array([7, 1])
# Solve by inversion, and with the recommended solver
x_inv = np.linalg.inv(A) @ b
x_solve = np.linalg.solve(A, b)
print("solution via inverse:", x_inv)
print("solution via solve :", x_solve)solution via inverse: [2.42857143 0.71428571]
solution via solve : [2.42857143 0.71428571]
Prefer solve over inv
solve over invnp.linalg.solve(A, b) is both faster and more numerically accurate than forming np.linalg.inv(A) @ b. Inverting a matrix explicitly is almost never necessary and should be avoided in real calculations; we show the inverse here only because it makes the algebra transparent.
Eigenvalues and eigenvectors¶
When a matrix acts on a vector it usually changes both its length and direction. Certain special vectors keep their direction and are only rescaled. These are eigenvectors, and the scaling factors are eigenvalues.
Rearranging to , a nonzero solution exists only when the characteristic equation holds:
The sign and size of say what happens to the eigenvector: stretches it, compresses it, flips it, and collapses it. In quantum mechanics this exact structure reappears for operators acting on wavefunctions: eigenvalues are the measurable observables (energies, momenta) and eigenvectors are the states with definite outcomes. The next page makes that leap explicit, showing that a function is just a vector with infinitely many components and the Schrodinger equation is this same eigenvalue problem .
import numpy as np
A = np.array([[2, 1],
[1, 2]])
vals, vecs = np.linalg.eig(A)
print("eigenvalues :", vals)
print("eigenvectors (columns):\n", vecs)
# Verify A v = lambda v for the first eigenpair
print("\nA @ v0 :", A @ vecs[:, 0])
print("lambda0 * v0 :", vals[0] * vecs[:, 0])eigenvalues : [3.+0.j 1.+0.j]
eigenvectors (columns):
[[ 0.70710678+0.j -0.70710678+0.j]
[ 0.70710678+0.j 0.70710678+0.j]]
A @ v0 : [2.12132034+0.j 2.12132034+0.j]
lambda0 * v0 : [2.12132034+0.j 2.12132034+0.j]
The whole story in one picture: a matrix turns almost every vector, but an eigenvector comes back parallel to itself.
Source
A_eig = np.array([[2, 1], [1, 2]])
w = np.array([1.0, -0.35])
u = np.array([1.0, 1.0]) / np.sqrt(2)
fig_e, (axg, axe) = plt.subplots(1, 2, figsize=(9, 4.4))
for ax_, v1, v2, c1, c2, ttl in [
(axg, w, A_eig @ w, "0.45", "steelblue", "a generic vector turns"),
(axe, u, A_eig @ u, "0.45", "crimson", "an eigenvector only stretches"),
]:
ax_.annotate("", xy=v1, xytext=(0, 0), arrowprops=dict(arrowstyle="->", color=c1, lw=2.5))
ax_.annotate("", xy=v2, xytext=(0, 0), arrowprops=dict(arrowstyle="->", color=c2, lw=2.5))
ax_.text(*(v1 * 1.15), r"$\vec{v}$", color=c1, fontsize=13)
ax_.text(*(v2 * 1.06), r"$A\vec{v}$", color=c2, fontsize=13)
ax_.set_xlim(-0.6, 3.0); ax_.set_ylim(-1.2, 2.6)
ax_.set_aspect("equal"); ax_.grid(True, ls=":", alpha=0.6)
ax_.set_title(ttl, fontsize=11)
fig_e.tight_layout()
plt.show()
On the left, (gray) and (blue) point in different directions: the matrix rotated the vector. On the right, the eigenvector obeys : same direction, three times longer. Eigenvectors are the directions along which a matrix acts like simple multiplication.
Solution
gives : the two possible outcomes of the measurement. For : ; for : . These are exactly the spin-right and spin-left states of Chapter 5, and note they are orthogonal, as eigenvectors of distinct eigenvalues of a symmetric matrix must be.
The geometric action of a matrix¶
Different kinds of matrices deform a shape in characteristic ways:
| Matrix type | Example | Effect |
|---|---|---|
| Diagonal, unequal entries | [[1.8, 0], [0, 0.8]] | Stretch, different scaling per axis |
| Diagonal, equal entries | [[0.7, 0], [0, 0.7]] | Uniform scaling |
| Rotation (orthogonal) | [[cosθ, -sinθ], [sinθ, cosθ]] | Rotation, preserves lengths and angles |
| Off-diagonal term | [[1, k], [0, 1]] | Shear, slants without changing area |
We apply each matrix to the same small polygon to see how it transforms.
Source
import numpy as np
import matplotlib.pyplot as plt
coords = np.array([[0, 0], [0.5, 0.5], [0.5, 1.5], [0, 1], [0, 0]]).T
stretch = np.array([[1.8, 0], [0, 0.8]])
rotation = np.array([[np.cos(np.pi / 6), -np.sin(np.pi / 6)],
[np.sin(np.pi / 6), np.cos(np.pi / 6)]])
shear = np.array([[1, 0.8], [0, 1]])
scaling = np.array([[0.7, 0], [0, 0.7]])
transforms = {
"Stretch (non-uniform)": stretch,
"Rotation (30 deg)": rotation,
"Shear": shear,
"Scaling (uniform)": scaling,
}
fig, axes = plt.subplots(2, 2, figsize=(8, 8))
for ax, (name, M) in zip(axes.flat, transforms.items()):
ax.plot(coords[0], coords[1], 'r--', label='original')
new_coords = M @ coords
ax.plot(new_coords[0], new_coords[1], 'b-', label='transformed')
ax.set_title(name, fontsize=11)
ax.set_aspect('equal', 'box')
ax.set_xlim(-2, 2)
ax.set_ylim(-1, 2)
ax.grid(True, ls=':')
ax.legend(fontsize=8, loc='upper left')
plt.suptitle('Fig.2 How different matrices deform the same shape', fontsize=13)
plt.tight_layout()
plt.show()
A matrix in new axes: diagonalization¶
Turning the axes changes the components of a vector but not the vector, as in the tip on bases at the start of this page. A matrix is the same: its entries depend on the axes, while what it does to an arrow does not.
In the usual axes and , the entry in row and column is a dot product, : the matrix turns into , and the entry says how much of that lands on . Turn to any two perpendicular unit vectors and and the same recipe gives the matrix in the new axes. In Dirac’s notation it is a “sandwich” of a bra, the matrix and a ket:
The matrix has not changed: it turns and stretches every arrow exactly as before. Only its description changes.
Read column of as the arrow written in the new axes. The diagonal entry is the part of along its own axis; the off-diagonal entry is the part that lands on the other axis.
Try it: drag on the plane to turn the axes for , the matrix of the eigenvalue picture above. The orange pieces are the off-diagonal entries. They shrink as you turn, and at they vanish: each then lies along its own axis. The trace and the determinant never change.
For this , axes turned by an angle , with and , give
The off-diagonal entry vanishes at . The trace is 4 and the determinant is at every angle, so the eigenvalues, the roots of , cannot change when the axes turn.
Example: diagonal in its own eigenbasis
Write in the axes of its eigenvectors, and .
Each eigenvector is only stretched, and , so every sandwich is an eigenvalue times an overlap, . The axes are perpendicular unit vectors, so the overlap is 1 for and 0 otherwise:
Check one entry by row times column: , so . The trace is still and the determinant .
Finding those axes is called diagonalizing the matrix. A symmetric matrix always has perpendicular unit eigenvectors (so does a Hermitian one, see Operators as Matrices), so it can always be diagonalized. In numpy, eigh returns the eigenvectors as the columns of a matrix U, and U.T @ A @ U computes every sandwich at once: row of U.T is the bra, column of U the ket.
import numpy as np
A_ax = np.array([[2.0, 1.0], [1.0, 2.0]])
lam_ax, U_ax = np.linalg.eigh(A_ax) # columns of U_ax: the eigenvectors, the new axes
A_ax_new = U_ax.T @ A_ax @ U_ax # every sandwich at once
print("eigenvalues :", lam_ax) # eigh lists them in increasing order
print("A in the new axes :\n", np.round(A_ax_new, 10) + 0.0) # adding 0.0 prints -0. as 0.
print("trace before, after:", np.trace(A_ax), round(np.trace(A_ax_new), 10))
print("det before, after:", round(np.linalg.det(A_ax), 10), round(np.linalg.det(A_ax_new), 10))eigenvalues : [1. 3.]
A in the new axes :
[[1. 0.]
[0. 3.]]
trace before, after: 4.0 4.0
det before, after: 3.0 3.0
The new matrix is diagonal, with the eigenvalues 1 and 3 in increasing order, and the trace and determinant are unchanged. This is what “diagonalizing a Hamiltonian” means in Operators 1: find the eigenvectors of , write in those axes, and read the energies off the diagonal. For complex matrices the bra carries a complex conjugate, U.conj().T @ A @ U.
Problems¶
Problem 1: Dot product and angle¶
Find and the angle between and .
Problem 2: Normalize a vector¶
Normalize .
Solution
, so the normalized vector is , which has length 1.
Problem 3: Check orthogonality¶
Are and orthogonal? Are they orthonormal?
Solution
Their dot product is , so they are orthogonal. Each has length , not 1, so they are orthogonal but not orthonormal. Dividing each by makes them orthonormal.
Problem 4: Eigenvalues by hand¶
Find the eigenvalues of .
Solution
so and , matching the numerical output above.
Problem 5: Solve a linear system¶
Solve by hand and check against the code.
Problem 6: Rotation preserves length¶
Show that the rotation matrix leaves the norm of any vector unchanged.
Problem 7: Eigenvector direction¶
Verify that is an eigenvector of and identify its eigenvalue.
Problem 8: Complex inner product¶
For complex vectors the inner product conjugates the first entry: . Compute for and confirm it is real and positive.