Structure of each exercise: problem + hint (with a solution dropdown) → Python numerical verification (with execution output)
Some verification code blocks are shared by several exercises.
import numpy as np
np.set_printoptions(precision=4, suppress=True, linewidth=100)
# ── Visual hierarchy helpers ──────────────────────────────
def print_header(title):
print("=" * 60)
print(f" {title}")
print("=" * 60)
def print_step(step, desc):
print(f"\n▶ Step {step}: {desc}")
print("-" * 40)
def compare_print(label, actual, expected_desc):
print(f"\n[{label}]")
print(f" Computed value: {actual}")
print(f" Expected value: {expected_desc}")
# ── Linear algebra utilities ─────────────────────────────
def rref(A, tol=1e-10):
"""Reduce A to RREF; return (RREF matrix, pivot column indices)"""
M = A.astype(float).copy()
r, pivot_cols = 0, []
for c in range(M.shape[1]):
idx = np.argmax(np.abs(M[r:, c])) + r
if abs(M[idx, c]) < tol:
continue
M[[r, idx]] = M[[idx, r]]
M[r] /= M[r, c]
for i in range(len(M)):
if i != r:
M[i] -= M[i, c] * M[r]
pivot_cols.append(c)
r += 1
if r == M.shape[0]:
break
return M, pivot_cols
def rank(A):
_, piv = rref(np.array(A, dtype=float))
return len(piv)
def null_basis(A, tol=1e-10):
"""Return a basis of the null space of A"""
R, piv = rref(np.array(A, dtype=float))
n = A.shape[1]
free = [c for c in range(n) if c not in piv]
basis = []
for f in free:
v = np.zeros(n)
v[f] = 1.0
for row, p in enumerate(piv):
if row < R.shape[0]:
v[p] = -R[row, f]
basis.append(v)
return basis
print("Helper functions loaded")
§4.1.1 Vector Spaces, Subspaces, and Spans¶
Vector Space Axioms and Subspaces¶
# Python verification: exer-ch4-tf-axioms
# Paired with the exercise label above; numerical examples do not replace a general proof.
import numpy as np
# (a) u + v is still in R^3
u = np.array([1, 3, -2])
v = np.array([4, -3, -2])
print("u + v =", u + v, "→ in R^3 ✓")
# (b) 0 * v = 0
v_gen = np.array([3.7, -2.1, 5.5])
print("0 * v =", 0 * v_gen, "→ the zero vector ✓")
# (d) Uniqueness of the negative: taking v=0 gives -v=0=v
zero = np.zeros(3)
print("-0 =", -zero, "= 0, so -v can equal v ✓ (when v=0)")
# (f) A counterexample for c*v=0
c = 2.0
v_zero = np.zeros(4)
print(f"c={c}, v=0: c*v = {c * v_zero} = 0, but c ≠ 0")
# (h) From u+w = v+w the conclusion is u=v, not w=0
u2 = np.array([1, 0])
v2 = np.array([1, 0])
w2 = np.array([3, 5])
print("u+w =", u2+w2, "= v+w =", v2+w2, "→ u=v, but w ≠ 0")
# Python verification: exer-ch4-subspace-axioms
# Paired with the exercise label above; numerical examples do not replace a general proof.
# W0: integer lattice points — a counterexample to closure under scalar multiplication
v_int = np.array([1, 1])
c_frac = 0.5
print(f"W0: {c_frac} * {v_int} = {c_frac * v_int} ← not an integer vector, so W0 is not a subspace")
# W2: union of the two coordinate axes — a counterexample to closure under addition
v1_ax = np.array([1, 0])
v2_ax = np.array([0, 1])
s = v1_ax + v2_ax
print(f"W2: {v1_ax} + {v2_ax} = {s}, product xy = {s[0]*s[1]} ≠ 0 → not in W2")
# W3: the plane x+y+z=1 — does not contain the zero vector
check_zero = 0 + 0 + 0
print(f"W3: 0+0+0={check_zero} ≠ 1 → the zero vector is not in W3, so W3 is not a subspace")
# W1: the line y=x — check closure
t1, t2, c = 2, 3, -1.5
v_w2_1 = np.array([t1, t1])
v_w2_2 = np.array([t2, t2])
print(f"W1 addition: {v_w2_1} + {v_w2_2} = {v_w2_1+v_w2_2}, still on y=x ✓")
print(f"W1 scalar: {c} * {v_w2_1} = {c * v_w2_1}, still on y=x ✓")
# W5: the xy-plane
w6_v = np.array([1.2, -3.4, 0])
w6_u = np.array([5.6, 0.7, 0])
c6 = 3.0
print(f"W5 addition: {w6_v} + {w6_u} = {w6_v+w6_u}, third component = 0 ✓")
print(f"W5 scalar: {c6} * {w6_v} = {c6*w6_v}, third component = 0 ✓")
# Python verification: exer-ch4-subspace-check
# Paired with the exercise label above; numerical examples do not replace a general proof.
# W1: a+2b = c+3d is closed under addition and scalar multiplication
def check_W1(v):
a, b, c, d = v
return np.isclose(a + 2*b, c + 3*d)
v1 = np.array([1.0, 2.0, 5.0, 0.0]) # 1+4=5, 5+0=5 ✓
v2 = np.array([3.0, 0.0, 0.0, 1.0]) # 3+0=3, 0+3=3 ✓
print(f"v1 in W1: {check_W1(v1)}, v2 in W1: {check_W1(v2)}")
print(f"v1+v2 in W1: {check_W1(v1+v2)}")
print(f"2.5*v1 in W1: {check_W1(2.5*v1)}")
# W4: a=b or c=d — a counterexample to closure under addition
u4 = np.array([1.0, 1.0, 0.0, 1.0]) # a=b ✓
v4 = np.array([0.0, 1.0, 1.0, 1.0]) # c=d ✓
s4 = u4 + v4
def check_W4(v):
a, b, c, d = v
return np.isclose(a, b) or np.isclose(c, d)
print(f"\nW4: u in W4={check_W4(u4)}, v in W4={check_W4(v4)}")
print(f"u+v={s4}, in W4={check_W4(s4)} ← counterexample, so W4 is not a subspace")
# W6: the kernel Av=0
A6 = np.array([[1,2,3],[0,1,2],[0,0,0]], dtype=float)
null_vecs = null_basis(A6)
print(f"\nW6 (ker A): basis vectors = {null_vecs}")
# Check that a linear combination stays in the kernel
if null_vecs:
combo = 2*null_vecs[0]
print(f"2*ker_vec in kernel: {np.allclose(A6 @ combo, 0)}")
Spans¶
# Python verification: exer-ch4-span-basic
# Paired with the exercise label above; numerical examples do not replace a general proof.
# (a) span{(2,3)}
v = np.array([2, 3])
for w, name in [([4,6],'(4,6)'), ([-4,-6],'(-4,-6)'), ([1,1],'(1,1)')]:
w = np.array(w, dtype=float)
# solve c*v = w
if not np.isclose(v[1]*w[0], v[0]*w[1]):
print(f"{name} ∉ span{{v}}")
else:
c = w[0]/v[0] if not np.isclose(v[0], 0) else w[1]/v[1]
print(f"{name} = {c:.4f} * v ∈ span{{v}}")
# (d) Vectors in span{u, v}
u7d = np.array([1.0, 1.0, 0.0])
v7d = np.array([-1.0, 1.0, 1.0])
A7d = np.column_stack([u7d, v7d])
for w, name in [([1,0,0],'(1,0,0)'), ([0,1,0],'(0,1,0)'), ([0,0,1],'(0,0,1)')]:
w = np.array(w, dtype=float)
try:
x, res, _, _ = np.linalg.lstsq(A7d, w, rcond=None)
in_span = np.isclose(A7d @ x, w).all()
print(f"{name} ∈ span{{u,v}}: {in_span}")
except:
print(f"{name}: computation error")
# Python verification: exer-ch4-span-check-r3, exer-ch4-span-r4
# Paired with the exercise label above; numerical examples do not replace a general proof.
# Q8: u=(1,2,-1), v=(0,1,1), b=(3,-1,1)
u8 = np.array([1.0, 2.0, -1.0])
v8 = np.array([0.0, 1.0, 1.0])
b8 = np.array([3.0, -1.0, 1.0])
A8 = np.column_stack([u8, v8])
x8, _, _, _ = np.linalg.lstsq(A8, b8, rcond=None)
print("R³ problem: b = A8 x8?", np.isclose(A8 @ x8, b8).all(), "→ b is not in span{u,v}")
print(" inner products: b·u =", np.dot(b8, u8), ", b·v =", np.dot(b8, v8), "(both zero: perpendicularity verified ✓)")
# Q9: u=(1,1,1,0), v=(0,1,-1,1)
u9 = np.array([1.0, 1.0, 1.0, 0.0])
v9 = np.array([0.0, 1.0, -1.0, 1.0])
A9 = np.column_stack([u9, v9])
for w, name in [([2,5,-1,3],'(2,5,-1,3)'), ([0,1,0,0],'(0,1,0,0)')]:
w = np.array(w, dtype=float)
x, _, _, _ = np.linalg.lstsq(A9, w, rcond=None)
in_span = np.isclose(A9 @ x, w).all()
print(f"R⁴ problem: {name} ∈ span{{u,v}}: {in_span}", end="")
if in_span:
print(f" (coefficients: {x.round(4)})")
else:
print()
# Python verification: exer-ch4-span-r3-full
# Paired with the exercise label above; numerical examples do not replace a general proof.
v0 = np.array([1.0, 1.0, 0.0])
v1 = np.array([0.0, 1.0, 1.0])
v2 = np.array([1.0, 0.0, 1.0])
M = np.column_stack([v0, v1, v2])
d = np.linalg.det(M)
print(f"det[v0, v1, v2] = {d:.4f} ({'≠ 0, so they span R³ ✓' if abs(d) > 1e-10 else '= 0'})")
print(f"rank = {rank(M)}")
§4.1.2 Linear Independence, Bases, and Dimension¶
Linear Independence and Dependence¶
# Python verification: exer-ch4-lindep-check
# Paired with the exercise label above; numerical examples do not replace a general proof.
groups = {
"0: (2,5)": [[2,5]],
"1: (1,1),(-1,1)": [[1,1],[-1,1]],
"2: (1,1),(-1,-1)": [[1,1],[-1,-1]],
"4: (1,3,5),(-2,-6,-10)": [[1,3,5],[-2,-6,-10]],
"8: (1,0,0,1),(0,1,1,0),(1,1,1,1)": [[1,0,0,1],[0,1,1,0],[1,1,1,1]],
"10: four vectors in R⁴": [[1,1,0,0],[0,1,1,0],[0,0,1,1],[1,0,0,1]],
}
for name, vecs in groups.items():
M = np.array(vecs, dtype=float)
r = rank(M)
n_vecs = len(vecs)
status = "linearly independent" if r == n_vecs else f"linearly dependent (rank={r})"
print(f" {name}: {status}")
Bases and Dimension¶
# Python verification: exer-ch4-basis-check
# Paired with the exercise label above; numerical examples do not replace a general proof.
# (a) Are (2,7),(3,-2) a basis of R^2?
A_a = np.array([[2,3],[7,-2]], dtype=float)
print(f"(a) det = {np.linalg.det(A_a):.4f} → {'a basis ✓' if abs(np.linalg.det(A_a)) > 1e-10 else 'not a basis'}")
# (c) Are (1,0,0),(1,1,0),(1,1,1) a basis of R^3?
A_c = np.array([[1,1,1],[0,1,1],[0,0,1]], dtype=float)
print(f"(c) det = {np.linalg.det(A_c):.4f} → {'a basis ✓' if abs(np.linalg.det(A_c)) > 1e-10 else 'not a basis'}")
# (d) W = {x+y+z=0}: are the three vectors a basis?
vecs_d = np.array([[1,-1,0],[0,-1,1],[1,0,-1]], dtype=float)
r_d = rank(vecs_d)
print(f"(d) rank = {r_d} (dim W = 2); the three vectors are linearly dependent ({'yes' if r_d < 3 else 'no'}), so they are not a basis")
# (f) W = span{(1,0,1,0),(0,1,0,1)}: check both bases
B1 = np.array([[1,0,1,0],[0,1,0,1]], dtype=float)
B2 = np.array([[1,1,1,1],[-1,1,-1,1]], dtype=float)
print(f"(f) B1 rank = {rank(B1)}, B2 rank = {rank(B2)}")
# Check that B2 lies in W
print(f" (1,1,1,1) = (1,0,1,0)+(0,1,0,1): {np.allclose(B2[0], B1[0]+B1[1])}")
print(f" (-1,1,-1,1) = -(1,0,1,0)+(0,1,0,1): {np.allclose(B2[1], -B1[0]+B1[1])}")
# Python verification: exer-ch4-find-basis
# Paired with the exercise label above; numerical examples do not replace a general proof.
# (a) {x+y+z=0}
A_a = np.array([[1,1,1]], dtype=float)
null_a = null_basis(A_a)
print(f"(a) ker basis: {null_a}, dim={len(null_a)}")
# (b) {x-2y=0, 3y+2z=0}
A_b = np.array([[1,-2,0],[0,3,2]], dtype=float)
null_b = null_basis(A_b)
print(f"(b) ker basis: {null_b}, dim={len(null_b)}")
# (h) span{(1,1,0,0),(0,1,1,0),(0,0,1,1),(1,0,0,1)}
M_h = np.array([[1,1,0,0],[0,1,1,0],[0,0,1,1],[1,0,0,1]], dtype=float)
r_h = rank(M_h)
R_h, piv_h = rref(M_h)
print(f"(h) rank={r_h}, pivots in columns {piv_h}")
print(f" RREF:\n{R_h}")
Sums and Direct Sums of Subspaces (Extension)¶
# Python verification: exer-ch4-subspace-sum
# Paired with the exercise label above; numerical examples do not replace a general proof.
# (b) U=span{(1,0,0)}, W=span{(0,0,1)}
U_b = np.array([[1.0,0.0,0.0]])
W_b = np.array([[0.0,0.0,1.0]])
UW_b = np.vstack([U_b, W_b])
print(f"(b) dim(U)={rank(U_b)}, dim(W)={rank(W_b)}, dim(U+W)={rank(UW_b)}")
print(f" dimension formula check: {rank(U_b)}+{rank(W_b)}-dim(U∩W)={rank(UW_b)}")
# U∩W = {0} so dim(U∩W)=0
# (c) U=span{(0,1,2)}, W=span{(0,1,0),(0,0,1)}
U_c = np.array([[0.0,1.0,2.0]])
W_c = np.array([[0.0,1.0,0.0],[0.0,0.0,1.0]])
# (0,1,2) = 1*(0,1,0) + 2*(0,0,1) ∈ W
print(f"\n(c) (0,1,2) ∈ W: {np.isclose(np.linalg.lstsq(W_c.T, U_c.T, rcond=None)[1], 0).all()}")
check = U_c[0] - 1*W_c[0] - 2*W_c[1]
print(f" (0,1,2) - (0,1,0) - 2*(0,0,1) = {check} (= 0 → U ⊂ W)")
UW_c = np.vstack([U_c, W_c])
print(f" dim(U)={rank(U_c)}, dim(W)={rank(W_c)}, dim(U+W)={rank(UW_c)}")
# Python verification: exer-ch4-direct-sum
# Paired with the exercise label above; numerical examples do not replace a general proof.
# (d) U = {x+2y+3z=0}, W = span{(1,2,3)}
A_U = np.array([[1,2,3]], dtype=float)
null_U = null_basis(A_U)
print(f"Basis of U (ker of [1,2,3]):")
for v in null_U:
print(f" {v}")
w_vec = np.array([1.0, 2.0, 3.0])
print(f"\nW = span{{(1,2,3)}}")
# Check that w ∉ U
print(f"(1,2,3) ∈ U? (1+4+9={1+4+9} ≠ 0): {'no ✓'}")
# dim(U)+dim(W) = 2+1 = 3 = dim(R^3)
UW = np.vstack([np.array(null_U), w_vec])
print(f"dim(U+W) = rank of combined = {rank(UW)} = dim(R³) ✓")
print(f"The direct sum holds: U ∩ W = {{0}}, U + W = R³ ✓")
Comprehensive True-or-False Questions¶
§4.2.1 Linear Mappings, Kernels, and Images¶
# Python verification: exer-ch4-linmap-check
# Paired with the exercise label above; numerical examples do not replace a general proof.
import numpy as np
# T0: T0(x,y) = (2x-y, x+3y) — linear
def T0(v): return np.array([2*v[0]-v[1], v[0]+3*v[1]])
u, v, c = np.array([1.0,2.0]), np.array([-1.0,3.0]), 2.5
print("T0 linear:", np.allclose(T0(c*u + v), c*T0(u) + T0(v)), "✓")
# T1: T1(x,y) = (x+1, y-1) — not linear (T1(0) ≠ 0)
def T1(v): return np.array([v[0]+1, v[1]-1])
print("T1(0,0) =", T1(np.zeros(2)), "≠ (0,0) → not a linear mapping ✓")
# T2: T2(x,y,z) = (x^2, y+z) — not linear
def T2(v): return np.array([v[0]**2, v[1]+v[2]])
u2 = np.array([1.0,0.0,0.0])
print("T2(2u)=", T2(2*u2), ", 2*T2(u)=", 2*T2(u2), "→ not equal, so not linear ✓")
# T3: T3(x,y,z) = (y,z,0) — linear
A3 = np.array([[0,1,0],[0,0,1],[0,0,0]], dtype=float)
def T3(v): return A3 @ v
print("Matrix of T3:\n", A3)
print("T3 linear:", np.allclose(T3(c*u2 + np.array([0.5,1.5,2.5])),
c*T3(u2) + T3(np.array([0.5,1.5,2.5]))), "✓")
# T4: |w| — not linear
def T4(v): return np.array([v[0], abs(v[3])])
v4 = np.array([0,0,0,1.0])
print("T4(-v)=", T4(-v4), ", -T4(v)=", -T4(v4), "→ not equal ✓")
# Python verification: exer-ch4-ker-im-matrix
# Paired with the exercise label above; numerical examples do not replace a general proof.
cases = [
("(1)", np.array([[1,2],[2,4]], dtype=float), 2),
("(2)", np.array([[1,0,2],[0,1,3],[0,0,0]], dtype=float), 3),
("(3)", np.array([[1,2,3],[2,4,6]], dtype=float), 3),
]
for label, A, n in cases:
r = rank(A)
nul = n - r
print(f"Case {label}: rank={r}, nullity={nul}, rank+nullity={r+nul}={n} ✓")
nb = null_basis(A)
print(f" ker basis: {nb}")
# Im: use RREF to find pivot column indices, then take the corresponding columns of the original A
R, piv = rref(A)
print(f" Im: span of columns {piv} of A = {A[:,piv].T.tolist()}")
print()
# Python verification: exer-ch4-shift-power
# Paired with the exercise label above; numerical examples do not replace a general proof.
A = np.array([[0,1,0],[0,0,1],[0,0,0]], dtype=float)
A2 = A @ A
A3 = A @ A @ A
print("Matrix of T: A =\n", A)
print("\nT^2 =\n", A2)
print("T^3 =\n", A3, "← the zero matrix (nilpotent)")
for power, M in enumerate([A, A2, A3], 1):
r = rank(M)
nul = 3 - r
print(f"\nT^{power}: rank={r}, nullity={nul}, sum={r+nul}=3 ✓")
print(f" ker basis: {null_basis(M)}")
§4.2.3–4.2.5 Change of Basis, Coordinates, and Matrix Representations¶
# Python verification: exer-ch4-coord-r4
# Paired with the exercise label above; numerical examples do not replace a general proof.
v = np.array([3.0, 1.0, 1.0, 2.0])
# Basis B0 (an orthogonal basis)
B0 = np.column_stack([(1, 1, 1, 1), (1, 1, -1, -1),
(1, -1, 1, -1), (-1, 1, 1, -1)]).astype(float)
coords0 = np.linalg.solve(B0, v)
print("Coordinates with respect to B0:", coords0.round(4))
print("Check: B0 @ coords0 =", (B0 @ coords0).round(4), "= v?", np.allclose(B0@coords0, v))
# Basis B1 (upper triangular)
B1 = np.column_stack([(1, 0, 0, 0), (1, 1, 0, 0),
(1, 1, 1, 0), (1, 1, 1, 1)]).astype(float)
coords1 = np.linalg.solve(B1, v)
print("\nCoordinates with respect to B1:", coords1.round(4))
print("Check: B1 @ coords1 =", (B1 @ coords1).round(4), "= v?", np.allclose(B1@coords1, v))
# Python verification: exer-ch4-change-of-basis-r2
# Paired with the exercise label above; numerical examples do not replace a general proof.
v56 = np.array([-2.0, 5.0])
# Bases B0 = {(1,1),(1,-1)}, B1 = {(1,1),(2,0)}
B0 = np.column_stack([(1, 1), (1, -1)]).astype(float)
B1 = np.column_stack([(1, 1), (2, 0)]).astype(float)
coords_B0 = np.linalg.solve(B0, v56)
coords_B1 = np.linalg.solve(B1, v56)
print(f"[v]_B0 = {coords_B0.round(4)}")
print(f"[v]_B1 = {coords_B1.round(4)}")
# Change-of-basis matrix P = B0^{-1} B1
P = np.linalg.inv(B0) @ B1
print(f"\nChange-of-basis matrix P = B0^{{-1}} B1 =\n{P.round(4)}")
# Check [v]_B0 = P [v]_B1
print(f"P @ [v]_B1 = {(P @ coords_B1).round(4)}")
print(f"[v]_B0 = {coords_B0.round(4)}")
print(f"Equal? {np.allclose(P @ coords_B1, coords_B0)}")
# Python verification: exer-ch4-represent-matrix-change
# Paired with the exercise label above; numerical examples do not replace a general proof.
# T: R^2 → R^2, A = [[0,1],[1,0]] (reflection across y=x)
A58 = np.array([[0.0,1.0],[1.0,0.0]])
B58 = np.column_stack([(1, 1), (-2, 2)]).astype(float)
# A_B = B^{-1} A B
A_B = np.linalg.inv(B58) @ A58 @ B58
print("A_B = B^{-1} A B =\n", A_B.round(4))
print("\nNote: (1,1) is fixed by the reflection (eigenvalue 1); reflecting (-2,2) across y=x gives (2,-2)=-1*(-2,2)")
print("Hence A_B = diag(1,-1):", np.allclose(A_B, np.diag([1,-1])))
§4.2.6 The Rank-Nullity Theorem¶
# Python verification: exer-ch4-rank-nullity
# Paired with the exercise label above; numerical examples do not replace a general proof.
cases_51 = [
("(1) T:R³→R²", np.array([[1,0,1],[0,1,1]], dtype=float), 3),
("(2) diag(0,1,4,9)", np.diag([0.0,1.0,4.0,9.0]), 4),
("(3) D (1/3 ones)", np.ones((3,3))/3, 3),
("(4) T:R²→R⁴", np.array([[1,-1],[2,-2],[3,-3],[4,-4]], dtype=float), 2),
]
for label, A, n in cases_51:
r = rank(A)
nul = n - r
print(f"{label}: rank={r}, nullity={nul}, sum={r+nul}={n} {'✓' if r+nul==n else '✗'}")
§4.3.1 Concrete Vector Spaces: Complex Numbers and Quaternions¶
# ============================================================
print_header("Basic Operations with Quaternions | exer-ch4-quaternion-basics")
# ============================================================
def q_mult(p, q):
"""Quaternion multiplication p*q, in the format (a,b,c,d) = a+bi+cj+dk"""
a0,b0,c0,d0 = p; a1,b1,c1,d1 = q
return np.array([a0*a1-b0*b1-c0*c1-d0*d1,
a0*b1+b0*a1+c0*d1-d0*c1,
a0*c1-b0*d1+c0*a1+d0*b1,
a0*d1+b0*c1-c0*b1+d0*a1])
def q_conj(p): return np.array([p[0], -p[1], -p[2], -p[3]])
def q_norm(p): return np.sqrt(np.sum(p**2))
def q_inv(p): return q_conj(p) / np.dot(p, p)
q0 = np.array([1.0, 1.0, 1.0, 1.0]) # 1+i+j+k
q1 = np.array([3.0, -1.0, 2.0, -1.0])
print_step(1, "Conjugate, norm, and unit quaternion of q₀ = 1+i+j+k")
print(" q₀ =", q0)
print(" q₀* =", q_conj(q0))
compare_print("‖q₀‖", f"{q_norm(q0):.6f}", "√4 = 2")
print(" q̂₀ =", (q0 / q_norm(q0)).round(6))
print_step(2, "Check the inverse of q₁")
q1_inv = q_inv(q1)
print(" q₁⁻¹ =", q1_inv.round(6))
compare_print("q₁·q₁⁻¹ (should be (1,0,0,0))", q_mult(q1, q1_inv).round(6), "(1,0,0,0) ✓")
print_step(3, "q₀+q₁ and q₀·q₁ (checking noncommutativity)")
print(" q₀ + q₁ =", q0 + q1)
print(" q₀ · q₁ =", q_mult(q0, q1))
print(" q₁ · q₀ =", q_mult(q1, q0))
compare_print("q₀q₁ = q₁q₀? (should be False)",
np.allclose(q_mult(q0, q1), q_mult(q1, q0)), "False → quaternion multiplication is not commutative ✓")
# Python verification: exer-ch4-quaternion-rotation
# Paired with the exercise label above; numerical examples do not replace a general proof.
# q = cos(θ) + sin(θ)k: check the rotation by 2θ about the k-axis
def q_mult(p, q):
a1,b1,c1,d1 = p; a2,b2,c2,d2 = q
return np.array([a1*a2-b1*b2-c1*c2-d1*d2,
a1*b2+b1*a2+c1*d2-d1*c2,
a1*c2-b1*d2+c1*a2+d1*b2,
a1*d2+b1*c2-c1*b2+d1*a2])
def q_conj(p): return np.array([p[0],-p[1],-p[2],-p[3]])
theta = np.pi / 6 # rotation by 2θ = π/3 = 60°
q = np.array([np.cos(theta), 0.0, 0.0, np.sin(theta)])
print(f"q = cos({theta:.4f}) + sin({theta:.4f})k = {q.round(4)}")
# pure imaginary quaternion (0,x,y,z)
w = np.array([0.0, 1.0, 0.0, 0.0]) # (the i direction)
result = q_mult(q_mult(q, w), q_conj(q))
print(f"T(i) = q*i*q̄ = {result.round(6)}")
print(f"Theoretical value (rotation by 2θ={2*theta:.4f} rad = {np.degrees(2*theta):.1f}°):")
print(f" i → cos(2θ)i + sin(2θ)j = ({np.cos(2*theta):.4f}, {np.sin(2*theta):.4f})")
# Matrix representation M_θ
two_theta = 2 * theta
M_theta = np.array([
[np.cos(two_theta), -np.sin(two_theta), 0],
[np.sin(two_theta), np.cos(two_theta), 0],
[0, 0, 1]
])
print(f"\nRotation matrix M_θ (rotation about the z-axis by {np.degrees(two_theta):.1f}°):")
print(M_theta.round(4))
§4.3.2 Matrix Spaces and Their Subspaces¶
# Python verification: exer-ch4-conj-transpose
# Paired with the exercise label above; numerical examples do not replace a general proof.
import numpy as np
cases_h = [
("(1)", np.array([[1,2],[3,4]], dtype=complex)),
("(2)", np.array([[2,1j],[-1j,0]], dtype=complex)),
("(3)", np.array([[1-1j,1j,1j],[0,3,1j],[0,0,4]], dtype=complex)),
]
for label, A in cases_h:
A_star = A.conj().T
is_hermite = np.allclose(A, A_star)
print(f"{label} A* =\n{A_star}")
print(f" Hermitian? {is_hermite}\n")
# Python verification: exer-ch4-hermite-check
# Paired with the exercise label above; numerical examples do not replace a general proof.
cases_herm = [
("A0", np.array([[3,2-1j],[2+1j,-1]], dtype=complex)),
("A1", np.array([[1,2],[2,3]], dtype=complex)),
("A2", np.array([[1,1j],[1j,-1]], dtype=complex)),
]
for name, A in cases_herm:
A_star = A.conj().T
is_hermite = np.allclose(A, A_star)
print(f"{name}* =\n{A_star}")
print(f" {name} Hermitian? {is_hermite}\n")
print("Expected: A0 and A1 are Hermitian, A2 is not")# Python verification: exer-ch4-sym-skew-direct-sum
# Paired with the exercise label above; numerical examples do not replace a general proof.
M64 = np.array([[1.0, 2.0],[0.0, 1.0]])
Sym = (M64 + M64.T) / 2
Skew = (M64 - M64.T) / 2
print("Original matrix M =\n", M64)
print("\nSymmetric part (A+A^T)/2 =\n", Sym)
print("Skew-symmetric part (A-A^T)/2 =\n", Skew)
print("\nSym + Skew = M?", np.allclose(Sym + Skew, M64))
print("Sym = Sym^T?", np.allclose(Sym, Sym.T))
print("Skew = -Skew^T?", np.allclose(Skew, -Skew.T))
# Python verification: exer-matrix-space-decomposition
# Paired with the exercise label above; numerical examples do not replace a general proof.
print_header("Direct-Sum Decomposition of Matrix Spaces | exer-matrix-space-decomposition")
print_step(1, "The hand-computed example: A = H + K")
A = np.array([[1+2j, 3],
[1j, 4-1j]])
A_star = A.conj().T # conjugate transpose A*
H = (A + A_star) / 2
K = (A - A_star) / 2
print("H = (A + A*)/2 =\n", H)
print("K = (A - A*)/2 =\n", K)
print("H + K = A ? ", np.allclose(H + K, A))
print("H* = H (Hermitian)? ", np.allclose(H.conj().T, H))
print("K* = -K (skew-Hermitian)?", np.allclose(K.conj().T, -K))
print_step(2, "A random complex matrix: the decomposition holds for every A")
rng = np.random.default_rng(0)
n = 4
B = rng.standard_normal((n, n)) + 1j * rng.standard_normal((n, n))
HB, KB = (B + B.conj().T) / 2, (B - B.conj().T) / 2
print("H + K = B ?", np.allclose(HB + KB, B),
"| H Hermitian?", np.allclose(HB.conj().T, HB),
"| K skew-Hermitian?", np.allclose(KB.conj().T, -KB))
print_step(3, "Real dimensions: dim Herm + dim Anti-Herm = dim M_n(C) = 2n²")
def herm_basis(n):
"""A real basis of Herm_n(C): E_ii, E_ij+E_ji, i(E_ij-E_ji) (i<j)"""
basis = []
for i in range(n):
E = np.zeros((n, n), dtype=complex); E[i, i] = 1
basis.append(E)
for i in range(n):
for j in range(i + 1, n):
S = np.zeros((n, n), dtype=complex); S[i, j] = S[j, i] = 1
T = np.zeros((n, n), dtype=complex); T[i, j] = 1j; T[j, i] = -1j
basis += [S, T]
return basis
def realify(M):
"""Regard M_n(C) as the real vector space R^{2n²}: concatenate (Re, Im)"""
return np.concatenate([M.real.ravel(), M.imag.ravel()])
HB_list = herm_basis(n)
KB_list = [1j * X for X in HB_list] # Anti-Herm = i · Herm
rH = np.linalg.matrix_rank(np.array([realify(X) for X in HB_list]))
rK = np.linalg.matrix_rank(np.array([realify(X) for X in KB_list]))
rHK = np.linalg.matrix_rank(np.array([realify(X) for X in HB_list + KB_list]))
compare_print("dim_R Herm_n", rH, f"n² = {n**2}")
compare_print("dim_R Anti-Herm_n", rK, f"n² = {n**2}")
compare_print("dim_R (Herm + Anti-Herm)", rHK, f"2n² = {2*n**2} (= dim_R M_n(C), and the intersection is {{0}})")
print_step(4, "The real case: dim Sym + dim Skew = n²")
dim_sym, dim_skew = n * (n + 1) // 2, n * (n - 1) // 2
compare_print("dim Sym_n + dim Skew_n", dim_sym + dim_skew, f"n² = {n**2}")§4.3.3 Function Spaces¶
§4.3.4 Composite Spaces¶
# Python verification: exer-netflix-tensor-vs-direct
# Paired with the exercise label above; numerical examples do not replace a general proof.
print_header("Recommendation Models: Direct Sum vs. Tensor Product | exer-netflix-tensor-vs-direct")
users = {"Xiaoming": np.array([1.0, 0.0]), "Xiaohong": np.array([0.0, 1.0]), "Xiaoli": np.array([0.5, 0.5])}
movies = {"Fast & Furious": np.array([1.0, 0.0]), "Titanic": np.array([0.0, 1.0]), "Mr. & Mrs. Smith": np.array([0.6, 0.4])}
print_step(1, "Question 1: tensor-product representations u ⊗ m (np.kron, flattened in C order)")
print(" Xiaoli ⊗ Mr. & Mrs. Smith =", np.kron(users["Xiaoli"], movies["Mr. & Mrs. Smith"]))
print(" Xiaoming ⊗ Titanic =", np.kron(users["Xiaoming"], movies["Titanic"]))
w = np.array([2.0, 3.0, 1.0, 2.0]) # weights of the direct-sum model
W = np.array([[5.0, 1.0],
[1.0, 4.0]]) # weight matrix of the tensor-product model
def r_direct(u, m):
return w @ np.concatenate([u, m]) # features (u, m) ∈ U ⊕ M
def r_tensor(u, m):
return u @ W @ m # u^T W m
print_step(2, "Question 3: Xiaoli's predicted rating for Mr. & Mrs. Smith")
u, m = users["Xiaoli"], movies["Mr. & Mrs. Smith"]
compare_print("Direct-sum model", r_direct(u, m), "2(0.5)+3(0.5)+0.6+2(0.4) = 3.9")
compare_print("Tensor-product model", r_tensor(u, m), "u^T W m = 2.8")
print(" The tensor-product model is also a linear model on U⊗M: vec(W)·(u⊗m) =", W.ravel() @ np.kron(u, m))
print_step(3, "Questions 2 and 4: one user's rating difference between an action movie and a romance movie")
for name in ["Xiaoming", "Xiaohong"]:
u = users[name]
d_dir = r_direct(u, movies["Fast & Furious"]) - r_direct(u, movies["Titanic"])
d_ten = r_tensor(u, movies["Fast & Furious"]) - r_tensor(u, movies["Titanic"])
print(f" {name}: direct-sum difference = {d_dir:+.1f}, tensor-product difference = {d_ten:+.1f}")
print("→ For every user the direct-sum model gives the difference w_2 - w_3 = -1, independent of the user;")
print(" the tensor-product difference changes sign with the user's preference, expressing the match of preference × genre.")
print_step(4, "Number of parameters: dim(U⊕M) = p+q vs dim(U⊗M) = pq")
for p, q in [(2, 2), (10, 20), (100, 1000)]:
print(f" p={p:>4}, q={q:>5}: direct sum {p+q:>6} weights, tensor product {p*q:>7} weights")# Python verification: exer-roulette-tensor
# Paired with the exercise label above; numerical examples do not replace a general proof.
# For readability, the bet types are simplified to 45: 38 straight-up bets (paying 35 to 1), red/black and odd/even (paying 1 to 1), and three dozen bets (paying 2 to 1).
print_header("Compressing One Day of Casino Roulette Data | exer-roulette-tensor")
# ── Outcome encoding: index 0 → "0", index 1 → "00", index k+1 → number k (k = 1..36) ──
n_out = 38
number = np.array([None, None] + list(range(1, 37)), dtype=object)
RED = {1, 3, 5, 7, 9, 12, 14, 16, 18, 19, 21, 23, 25, 27, 30, 32, 34, 36}
def wins(o, b):
"""Whether bet type b wins when the outcome is o"""
if b < 38:
return o == b # straight-up bet
k = number[o]
if k is None:
return False # 0 and 00 make every outside bet lose
return [k in RED, k not in RED, k % 2 == 1, k % 2 == 0,
1 <= k <= 12, 13 <= k <= 24, 25 <= k <= 36][b - 38]
odds = np.array([35] * 38 + [1, 1, 1, 1, 2, 2, 2]) # payout per unit stake on a win
n_bet = len(odds)
# G[o, b]: the casino's revenue per unit stake on bet type b when the outcome is o (losing bet +1, winning bet -odds)
G = np.array([[-odds[b] if wins(o, b) else 1 for b in range(n_bet)]
for o in range(n_out)], dtype=float)
print_step(1, "Simulating one day: individual transactions (raw data in Cartesian-product form)")
rng = np.random.default_rng(2024)
n_spin = 3000
outcome = rng.integers(0, n_out, size=n_spin)
rows = [] # (spin, outcome, bet type, amount)
for t in range(n_spin):
for _ in range(rng.poisson(6)):
rows.append((t, outcome[t], rng.integers(0, n_bet), rng.choice([10, 25, 50, 100, 500])))
rows = np.array(rows)
print(f" spins {n_spin}, bets {len(rows)}, raw data {rows.size} numbers")
net_raw = sum(amt * G[o, b] for _, o, b, amt in rows)
print(f" casino net revenue computed from the individual records = {net_raw:,.0f}")
print_step(2, "Tensor-product record: S = Σ_t e_{o_t} ⊗ s_t (a 38 × 45 matrix)")
S = np.zeros((n_out, n_bet))
np.add.at(S, (rows[:, 1], rows[:, 2]), rows[:, 3])
N = np.bincount(outcome, minlength=n_out) # number of occurrences of each outcome
compare_print("net revenue = ⟨G, S⟩_F = Σ G[o,b] S[o,b]", f"{np.sum(G * S):,.0f}", f"{net_raw:,.0f} (agrees with the individual records)")
red_rows = [o for o in range(n_out) if number[o] in RED]
print(f" net revenue when red comes up = {np.sum(G[red_rows] * S[red_rows]):,.0f}")
print(f" fraction of nonzero entries = {np.count_nonzero(S) / S.size:.1%} (with many spins, S is in fact nearly dense)")
print_step(3, "Direct-sum record: outcome counts ⊕ amount per bet type (38 + 45 dimensions)")
d = np.concatenate([N, S.sum(axis=0)])
print(f" length of the direct-sum vector = {d.size}")
print_step(4, "Counterexample: same marginals, different net revenue (2 outcomes × 2 bets, paying 1 to 1)")
G2 = np.array([[-1.0, 1.0], # red comes up: bets on red win, bets on black lose
[1.0, -1.0]]) # black comes up: bets on red lose, bets on black win
X = np.array([[100.0, 0.0], [0.0, 100.0]]) # scenario in which the bets hit
Y = np.array([[0.0, 100.0], [100.0, 0.0]]) # scenario in which the bets miss
for name, M in [("X", X), ("Y", Y)]:
print(f" {name}: outcome marginals {M.sum(axis=1)}, bet marginals {M.sum(axis=0)}, "
f"casino net revenue {np.sum(G2 * M):+.0f}")
print("→ The direct sum stores only the two sets of marginals and cannot tell X from Y; the tensor product S keeps the joint distribution and can.")
print_step(5, "Comparison of storage (counted as the number of stored values)")
print(f" Cartesian-product sampling (largest 1000 records × 5 fields, including player id): {1000 * 5}")
print(f" direct-sum statistics vector: {d.size}")
print(f" tensor-product matrix + outcome counts: {S.size + N.size}")✓ All exercises completed.