Scientific Computing (Python)
Chapter: 5.4 Scientific Computing Level: 3 — Production (from-scratch derivations, code-from-memory, explain-out-loud) Time limit: 45 minutes Total marks: 60
Write code from memory. Partial credit for correct structure. Where asked to "explain out loud," write 2–4 sentences of clear reasoning.
Question 1 — RK4 from scratch (12 marks)
(a) Derive the classical 4th-order Runge–Kutta update for , , writing out and the final combination. (4)
(b) Write a Python function rk4(f, y0, t0, t1, n) using NumPy that returns arrays (t, y) for a scalar or vector y0. (5)
(c) Explain out loud: why is RK4 called "4th-order," and how does the global error scale with step size ? (3)
Question 2 — Numerical integration from scratch (10 marks)
(a) Write the composite Simpson's rule formula for with (even) subintervals of width . (3)
(b) Implement simpson(f, a, b, n) in NumPy-vectorized form (no Python loop over points). Raise ValueError if n is odd. (5)
(c) Use your formula by hand to estimate with . Comment on the result. (2)
Question 3 — Root-finding from scratch (10 marks)
(a) Write the Newton–Raphson iteration formula and state one condition under which it fails to converge. (3)
(b) Implement newton(f, df, x0, tol=1e-10, maxit=100) returning the root; raise RuntimeError on non-convergence. (4)
(c) Perform 2 Newton iterations by hand for starting at . Report and as exact fractions. (3)
Question 4 — Broadcasting & vectorization (10 marks)
(a) State the two broadcasting rules NumPy uses to align shapes. (3)
(b) Given a of shape (3,1) and b of shape (1,4), what is the shape of a + b, and what does each output element equal? (2)
(c) A pairwise squared-distance matrix between rows of X (shape (m,d)) is needed. Write a vectorized one-liner (no Python loops) producing an (m,m) array using broadcasting. Explain out loud why it avoids loops. (5)
Question 5 — Floating point & linear algebra (10 marks)
(a) Explain out loud what catastrophic cancellation is, and give the numerically stable quadratic-formula root for the smaller-magnitude root of . (4)
(b) Why is scipy.linalg.solve(A, b) generally preferred over computing inv(A) @ b? Give two reasons. (3)
(c) For the system below, state what np.linalg.solve returns and give the exact solution:
(3)
Question 6 — FFT / SciPy submodules (8 marks)
(a) Explain out loud what np.fft.fft computes and what np.fft.fftfreq(n, d) returns. (3)
(b) Match each task to the correct SciPy submodule: (i) solving an ODE, (ii) curve fitting, (iii) LU decomposition, (iv) a t-test, (v) a sparse CSR matrix. (5)
Answer keyMark scheme & solutions
Question 1 (12)
(a) RK4 stages: (4, 1 each) Why: weighted average of slopes sampled at endpoints (weight 1) and midpoint (weight 2) matches Taylor expansion to .
(b) (5)
import numpy as np
def rk4(f, y0, t0, t1, n):
t = np.linspace(t0, t1, n+1)
h = (t1 - t0) / n
y0 = np.atleast_1d(np.asarray(y0, dtype=float))
y = np.empty((n+1, y0.size))
y[0] = y0
for i in range(n):
k1 = f(t[i], y[i])
k2 = f(t[i] + h/2, y[i] + h/2*k1)
k3 = f(t[i] + h/2, y[i] + h/2*k2)
k4 = f(t[i] + h, y[i] + h*k3)
y[i+1] = y[i] + h/6*(k1 + 2*k2 + 2*k3 + k4)
return t, yMarks: linspace/h (1), init/atleast_1d (1), 4 k-stages (2), update+return (1).
(c) (3) Local truncation error per step is ; accumulated over steps gives global error — hence "4th order." Halving reduces error ~16×.
Question 2 (10)
(a) (3) (odd indices weight 4, interior even weight 2, endpoints weight 1).
(b) (5)
import numpy as np
def simpson(f, a, b, n):
if n % 2 != 0:
raise ValueError("n must be even")
x = np.linspace(a, b, n+1)
h = (b - a) / n
y = f(x)
w = np.ones(n+1)
w[1:-1:2] = 4
w[2:-1:2] = 2
return h/3 * np.dot(w, y)Marks: even check (1), linspace/h (1), weight array (2), dot/return (1).
(c) (2) : nodes , . Estimate . Exact . Simpson is exact for cubics.
Question 3 (10)
(a) (3) . Fails when (division by zero) / near-flat derivative, or poor initial guess causing divergence/cycling.
(b) (4)
def newton(f, df, x0, tol=1e-10, maxit=100):
x = x0
for _ in range(maxit):
fx = f(x)
if abs(fx) < tol:
return x
x = x - fx / df(x)
raise RuntimeError("did not converge")Marks: loop (1), convergence check (1), update (1), raise (1).
(c) (3) . . . (, close to .)
Question 4 (10)
(a) (3) (1) Align shapes right-to-left; prepend 1's to the shorter shape. (2) In each dimension sizes must be equal or one of them must be 1 (the size-1 dim is stretched/broadcast).
(b) (2) Result shape (3,4). Element .
(c) (5)
D2 = ((X[:, None, :] - X[None, :, :])**2).sum(-1)Explain: X[:,None,:] is (m,1,d), X[None,:,:] is (1,m,d); broadcasting produces all differences as an (m,m,d) array in a single C-level operation, then summing over the last axis gives the (m,m) squared-distance matrix — no Python-level loop, so it runs in vectorized compiled code.
Question 5 (10)
(a) (4) Catastrophic cancellation: subtracting two nearly-equal floating-point numbers loses leading significant digits, hugely amplifying relative error. (2) Stable smaller root: compute , then roots are and ; choosing the avoids subtracting near-equal quantities. (2)
(b) (3) (1) Forming inv(A) is more expensive and less accurate (extra rounding); solve uses LU factorization directly. (2) solve is numerically more stable / better conditioned and preserves structure. (Either two valid reasons.)
(c) (3) Returns the solution vector . Solve: . . , . So .
Question 6 (8)
(a) (3) np.fft.fft computes the Discrete Fourier Transform: , returning complex amplitudes for each frequency bin. fftfreq(n, d) returns the sample frequencies (cycles per unit) in the FFT bin order, with spacing , including negative frequencies in the second half.
(b) (5, 1 each) (i) scipy.integrate — (ii) scipy.optimize — (iii) scipy.linalg — (iv) scipy.stats — (v) scipy.sparse.
[
{"claim": "Simpson n=2 estimate of int_0^2 x^3 = 4", "code": "h=1; xs=[0,1,2]; f=lambda x:x**3; est=h/3*(f(xs[0])+4*f(xs[1])+f(xs[2])); result = (est==4)"},
{"claim": "Newton x1=3/2, x2=17/2... =17/12 for x^2-2 from x0=1", "code": "from sympy import Rational; f=lambda x:x**2-2; df=lambda x:2*x; x0=Rational(1); x1=x0-f(x0)/df(x0); x2=x1-f(x1)/df(x1); result = (x1==Rational(3,2)) and (x2==Rational(17,12))"},
{"claim": "Solution of the 2x2 system is (4/5, 7/5)", "code": "A=Matrix([[2,1],[1,3]]); b=Matrix([3,5]); x=A.solve(b); result = (x[0]==Rational(4,5)) and (x[1]==Rational(7,5))"},
{"claim": "Broadcasting (3,1)+(1,4) gives shape (3,4)", "code": "import numpy as np; a=np.zeros((3,1)); b=np.zeros((1,4)); result = ((a+b).shape==(3,4))"}
]