Scientific Computing (Python)
Level 5 — Mastery (cross-domain: numerical analysis + physics + coding) Time limit: 2 hours 30 minutes Total marks: 60
Instructions: Answer all three questions. Code must be correct, idiomatic NumPy/SciPy where required, and free of Python loops unless a loop is intrinsic to the algorithm (e.g. time-stepping). Justify numerical claims. Use / for mathematics.
Question 1 — Catastrophic cancellation & a stable root finder (20 marks)
Consider solving the quadratic with , , in IEEE-754 double precision.
(a) The two exact roots satisfy and . Compute the two roots to leading order analytically, and identify which root, when computed by the "textbook" formula , suffers catastrophic cancellation. Explain why (relate to relative error and the number of significant digits lost). (6)
(b) Derive the numerically stable formula for both roots based on , giving , . Prove this avoids the cancellation. (5)
(c) Implement stable_roots(a, b, c) returning both roots via the method in (b). Then implement a Newton–Raphson solver newton(f, df, x0, tol=1e-14, maxit=100) from scratch and use it to refine the smaller-magnitude root of the same quadratic. State the theoretical order of convergence of Newton's method near a simple root and what happens at a double root. (6)
(d) For , estimate the relative error of the naive formula vs the stable formula in double precision (machine epsilon ). (3)
Question 2 — Vibrating system: eigenmodes, ODE integration, spectral check (22 marks)
A chain of 3 equal masses connected by identical springs (fixed–free–...–fixed at both ends) has stiffness matrix
(a) The normal-mode frequencies satisfy where are eigenvalues of . Using the known eigenvalues , , list and . Explain how you would obtain these numerically with np.linalg.eigh and why eigh is preferred over eig here. (5)
(b) Write the second-order system as a first-order system with . Give (block form). Show that the total mechanical energy is conserved by differentiating along trajectories. (6)
(c) Implement a from-scratch RK4 integrator rk4(f, y0, t0, tf, h) (fixed step) and integrate from , over . Show how you would call scipy.integrate.solve_ivp with DOP853 and rtol=1e-10 on the same problem, and describe the quantitative diagnostic you'd plot to compare energy drift of RK4 (with ) versus DOP853. (6)
(d) You now FFT the displacement of mass 1, , sampled at over . Predict the frequencies (in cycles per unit time) at which spectral peaks appear, and give the FFT bin resolution . Which subtopics (np.fft) determine that the highest resolvable frequency is the Nyquist frequency, and what is it here? (5)
Question 3 — Curve fitting, statistics & a stability argument (18 marks)
You measure decay data at times believed to follow .
(a) Explain why fitting this with scipy.optimize.curve_fit (Levenberg–Marquardt) can fail without a good initial guess, and give a principled way to obtain initial guesses for , , from the data. (4)
(b) curve_fit returns popt, pcov. State precisely what pcov is, how to extract the standard error on , and the assumption linking pcov to those errors. (4)
(c) Consider the normal-equations approach to linear least squares . Explain, using the condition number and SVD, why forming can lose accuracy, and why scipy.linalg.lstsq / QR is preferred. Relate . (6)
(d) You wish to test whether two samples of residuals come from the same distribution. Name the appropriate scipy.stats test and state the null hypothesis and how you'd interpret a p-value of at . (4)
Answer keyMark scheme & solutions
Question 1 (20)
(a) (6) By Vieta: , . So one root is large , the other small . Exactly: . (2)
- Large root: — computed by adding two nearly-equal positive quantities → no cancellation. (2)
- Small root using : subtraction of two nearly-equal numbers → catastrophic cancellation; only ~0 significant digits of the difference survive because the true difference is at the relative scale. Relative error blows up to . (2)
(b) (5) Define . Here the two terms and have the same sign, so the sum is an addition of like-signed numbers → no cancellation. (2) Then is the well-conditioned large root. The small root follows from the product , i.e. — obtained by division, which is backward-stable and introduces no cancellation. (2) Since here , , ; . (1)
(c) (6)
import numpy as np
def stable_roots(a, b, c):
disc = np.sqrt(b*b - 4*a*c)
q = -0.5*(b + np.sign(b)*disc)
return q/a, c/q # (large, small)
def newton(f, df, x0, tol=1e-14, maxit=100):
x = x0
for _ in range(maxit):
fx = f(x)
dx = fx/df(x)
x -= dx
if abs(dx) < tol:
break
return x
a,b,c = 1.0, -1e8, 1.0
big, small = stable_roots(a,b,c)
f = lambda x: a*x*x + b*x + c
df = lambda x: 2*a*x + b
root = newton(f, df, small) # refine the small root(3 for stable_roots, 2 for correct newton, 1 for refining the smaller-magnitude root.)
Order of convergence: quadratic () near a simple root. At a double root there, convergence degrades to linear (rate ). (1 embedded)
(d) (3) Stable formula: relative error a few (only rounding in division/sqrt). (1) Naive formula for the small root: cancellation amplifies relative error by ... concretely the subtraction loses ~16 digits, so the naive small root has relative error (often completely wrong / even ). (2)
Question 2 (22)
(a) (5) :
- : ,
- : ,
- : , (2)
: , , . (1)
Numerically: w2, V = np.linalg.eigh(K). eigh exploits that is real symmetric → guarantees real eigenvalues, orthonormal eigenvectors, and uses a more stable/efficient symmetric algorithm. eig (general) may return complex values with round-off imaginary parts and non-orthogonal vectors. (2)
(b) (6)
Energy: (using symmetry of : ). (2) Substitute : Energy conserved. (2)
(c) (6)
def rk4(f, y0, t0, tf, h):
n = int(round((tf-t0)/h))
t = t0; y = np.array(y0, float)
T=[t]; Y=[y.copy()]
for _ in range(n):
k1=f(t,y); k2=f(t+h/2,y+h/2*k1)
k3=f(t+h/2,y+h/2*k2); k4=f(t+h,y+h*k3)
y = y + h/6*(k1+2*k2+2*k3+k4); t += h
T.append(t); Y.append(y.copy())
return np.array(T), np.array(Y)
K = np.array([[2,-1,0],[-1,2,-1],[0,-1,2]], float)
A = np.block([[np.zeros((3,3)), np.eye(3)],[-K, np.zeros((3,3))]])
f = lambda t,y: A@y
y0 = np.array([1,0,0, 0,0,0], float)
T, Y = rk4(f, y0, 0, 20, 0.05)
from scipy.integrate import solve_ivp
sol = solve_ivp(f, (0,20), y0, method='DOP853', rtol=1e-10, atol=1e-12,
dense_output=True)(3 for correct RK4, 1 for solve_ivp call, 2 for diagnostic.) Diagnostic: plot (or relative drift) vs . Fixed-step RK4 shows monotone/secular energy drift per step accumulating; DOP853 with tight tol keeps drift near round-off. (2)
(d) (5)
is a superposition of the three modes → spectral peaks at :
, , cycles/unit time. (2)
Record length Hz bin spacing. (1)
Nyquist: np.fft/rfftfreq show max resolvable freq Hz. (2)
Question 3 (18)
(a) (4) The model is nonlinear in ; the sum-of-squares surface has flat/curved valleys, so LM can stall or diverge from a poor start. Principled guesses: (or last-time plateau); ; from a log-linear fit: , take slope. (4)
(b) (4) pcov is the estimated covariance matrix of popt. Standard error on : sigma_lambda = np.sqrt(np.diag(pcov))[idx_lambda]. It relies on the assumption that residuals are independent, (approximately) Gaussian, and — if absolute_sigma=False — that the model is correct so the residual variance is used to scale . (4)
(c) (6) Normal equations solve . Via SVD , , so singular values get squared: . (3) Squaring the condition number squares the error amplification; small singular values become negligible, losing accuracy (and may become numerically singular). QR/lstsq (SVD-based) work directly on , keeping conditioning at , hence more accurate/backward-stable. (3)
(d) (4) Two-sample Kolmogorov–Smirnov test: scipy.stats.ks_2samp. (Or Mann–Whitney/t-test — KS best for "same distribution".) : the two samples are drawn from the same distribution. p reject : significant evidence the distributions differ. (4)
[
{"claim":"Vieta small root of x^2-1e8 x+1 is ~1e-8 and stable formula c/q matches",
"code":"a,b,c=1.0,-1e8,1.0; import math; disc=math.sqrt(b*b-4*a*c); q=-0.5*(b+ (1 if b>0 else -1)*disc); small=c/q; result = abs(small-1e-8) < 1e-12"},
{"claim":"Eigenvalues of K are 2-