Level 5 — MasteryScientific Computing (Python)

Scientific Computing (Python)

2 minutes60 marksprintable — key stays hidden on paper

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 ax2+bx+c=0ax^2 + bx + c = 0 with a=1a=1, b=108b=-10^{8}, c=1c=1 in IEEE-754 double precision.

(a) The two exact roots satisfy x1x2=c/ax_1 x_2 = c/a and x1+x2=b/ax_1 + x_2 = -b/a. Compute the two roots to leading order analytically, and identify which root, when computed by the "textbook" formula x=b±b24ac2ax = \dfrac{-b \pm \sqrt{b^2-4ac}}{2a}, 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 q=12(b+sgn(b)b24ac)q = -\tfrac{1}{2}\left(b + \operatorname{sgn}(b)\sqrt{b^2-4ac}\right), giving x1=q/ax_1=q/a, x2=c/qx_2=c/q. 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 x2=c/qx_2 = c/q, estimate the relative error of the naive formula vs the stable formula in double precision (machine epsilon ε2.22×1016\varepsilon \approx 2.22\times10^{-16}). (3)


Question 2 — Vibrating system: eigenmodes, ODE integration, spectral check (22 marks)

A chain of 3 equal masses m=1m=1 connected by identical springs k=1k=1 (fixed–free–...–fixed at both ends) has stiffness matrix

K=(210121012),Mu¨=Ku,  M=I.K = \begin{pmatrix} 2 & -1 & 0 \\ -1 & 2 & -1 \\ 0 & -1 & 2 \end{pmatrix}, \qquad M\ddot{\mathbf{u}} = -K\mathbf{u},\; M=I.

(a) The normal-mode frequencies satisfy ωj2=λj\omega_j^2 = \lambda_j where λj\lambda_j are eigenvalues of KK. Using the known eigenvalues λj=22cos ⁣(jπ4)\lambda_j = 2 - 2\cos\!\big(\tfrac{j\pi}{4}\big), j=1,2,3j=1,2,3, list ωj2\omega_j^2 and ωj\omega_j. 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 y˙=Ay\dot{\mathbf{y}} = A\mathbf{y} with y=(u,u˙)\mathbf{y}=(\mathbf{u},\dot{\mathbf{u}}). Give AA (block form). Show that the total mechanical energy E=12u˙u˙+12uKuE=\tfrac12\dot{\mathbf u}^\top\dot{\mathbf u}+\tfrac12\mathbf u^\top K\mathbf u is conserved by differentiating EE along trajectories. (6)

(c) Implement a from-scratch RK4 integrator rk4(f, y0, t0, tf, h) (fixed step) and integrate from u(0)=(1,0,0)\mathbf u(0)=(1,0,0), u˙(0)=0\dot{\mathbf u}(0)=0 over t[0,20]t\in[0,20]. 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 h=0.05h=0.05) versus DOP853. (6)

(d) You now FFT the displacement of mass 1, u1(t)u_1(t), sampled at Δt=0.05\Delta t=0.05 over t[0,20]t\in[0,20]. Predict the frequencies (in cycles per unit time) at which spectral peaks appear, and give the FFT bin resolution Δf\Delta f. 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 yiy_i at times tit_i believed to follow y(t)=Aeλt+Cy(t)=A e^{-\lambda t} + C.

(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 AA, λ\lambda, CC from the data. (4)

(b) curve_fit returns popt, pcov. State precisely what pcov is, how to extract the 1σ1\sigma standard error on λ\lambda, and the assumption linking pcov to those errors. (4)

(c) Consider the normal-equations approach to linear least squares minxAxb2\min_x \|Ax-b\|_2. Explain, using the condition number and SVD, why forming AAA^\top A can lose accuracy, and why scipy.linalg.lstsq / QR is preferred. Relate κ(AA)=κ(A)2\kappa(A^\top A)=\kappa(A)^2. (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 0.0020.002 at α=0.05\alpha=0.05. (4)


Answer keyMark scheme & solutions

Question 1 (20)

(a) (6) By Vieta: x1+x2=b/a=108x_1+x_2 = -b/a = 10^8, x1x2=c/a=1x_1 x_2 = c/a = 1. So one root is large 108\approx 10^8, the other small 108\approx 10^{-8}. Exactly: b24ac=10164108(12×1016)\sqrt{b^2-4ac}=\sqrt{10^{16}-4}\approx 10^8(1-2\times10^{-16}). (2)

  • Large root: x+=108+2108x_+ = \frac{10^8 + \sqrt{\cdots}}{2}\approx 10^8 — computed by adding two nearly-equal positive quantities → no cancellation. (2)
  • Small root using bb24ac=108108(12×1016)-b-\sqrt{b^2-4ac} = 10^8 - 10^8(1-2\times10^{-16}): subtraction of two nearly-equal numbers → catastrophic cancellation; only ~0 significant digits of the difference survive because the true difference is at the 101610^{-16} relative scale. Relative error blows up to O(1)O(1). (2)

(b) (5) Define q=12(b+sgn(b)b24ac)q=-\tfrac12\big(b+\operatorname{sgn}(b)\sqrt{b^2-4ac}\big). Here the two terms bb and sgn(b)\operatorname{sgn}(b)\sqrt{\cdots} have the same sign, so the sum is an addition of like-signed numbers → no cancellation. (2) Then x1=q/ax_1=q/a is the well-conditioned large root. The small root follows from the product x1x2=c/ax_1 x_2=c/a, i.e. x2=c/qx_2=c/q — obtained by division, which is backward-stable and introduces no cancellation. (2) Since here b<0b<0, sgn(b)=1\operatorname{sgn}(b)=-1, q=12(bb24ac)=12(b+)108q=-\tfrac12(b-\sqrt{b^2-4ac})=\tfrac12(|b|+\sqrt{\cdots})\approx10^8; x2=1/q108x_2=1/q\approx10^{-8}. (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 (en+1Cen2|e_{n+1}|\le C|e_n|^2) near a simple root. At a double root f=0f'=0 there, convergence degrades to linear (rate 1/21/2). (1 embedded)

(d) (3) Stable formula: relative error \approx a few ε1016\varepsilon \sim 10^{-16} (only rounding in division/sqrt). (1) Naive formula for the small root: cancellation amplifies relative error by b/true difference108/1016\sim |b|/\text{true difference}\approx 10^8/10^{-16}... concretely the subtraction 10810810^8-10^8 loses ~16 digits, so the naive small root has relative error O(1)O(1) (often completely wrong / even 00). (2)


Question 2 (22)

(a) (5) λj=22cos(jπ/4)\lambda_j = 2-2\cos(j\pi/4):

  • j=1j=1: cos(π/4)=22\cos(\pi/4)=\tfrac{\sqrt2}{2}, λ1=220.5858\lambda_1 = 2-\sqrt2 \approx 0.5858
  • j=2j=2: cos(π/2)=0\cos(\pi/2)=0, λ2=2\lambda_2 = 2
  • j=3j=3: cos(3π/4)=22\cos(3\pi/4)=-\tfrac{\sqrt2}{2}, λ3=2+23.4142\lambda_3 = 2+\sqrt2 \approx 3.4142 (2)

ωj=λj\omega_j=\sqrt{\lambda_j}: ω10.7654\omega_1\approx0.7654, ω2=21.4142\omega_2=\sqrt2\approx1.4142, ω31.8478\omega_3\approx1.8478. (1)

Numerically: w2, V = np.linalg.eigh(K). eigh exploits that KK 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)

A=(0IK0),y˙=Ay, y=(uu˙).(2)A=\begin{pmatrix}0 & I\\ -K & 0\end{pmatrix},\qquad \dot{\mathbf y}=A\mathbf y,\ \mathbf y=\begin{pmatrix}\mathbf u\\ \dot{\mathbf u}\end{pmatrix}. \quad(2)

Energy: dEdt=u˙u¨+u˙Ku\dfrac{dE}{dt}= \dot{\mathbf u}^\top\ddot{\mathbf u} + \dot{\mathbf u}^\top K\mathbf u (using symmetry of KK: ddt12uKu=u˙Ku\tfrac{d}{dt}\tfrac12\mathbf u^\top K\mathbf u=\dot{\mathbf u}^\top K\mathbf u). (2) Substitute u¨=Ku\ddot{\mathbf u}=-K\mathbf u: dEdt=u˙(Ku)+u˙Ku=0.\dfrac{dE}{dt}= \dot{\mathbf u}^\top(-K\mathbf u)+\dot{\mathbf u}^\top K\mathbf u = 0. 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 E(t)E(0)|E(t)-E(0)| (or relative drift) vs tt. Fixed-step RK4 shows monotone/secular energy drift O(h4)O(h^4) per step accumulating; DOP853 with tight tol keeps drift near round-off. (2)

(d) (5) u1(t)u_1(t) is a superposition of the three modes → spectral peaks at fj=ωj/(2π)f_j=\omega_j/(2\pi): f10.1218f_1\approx0.1218, f20.2251f_2\approx0.2251, f30.2941f_3\approx0.2941 cycles/unit time. (2) Record length T=20Δf=1/T=0.05T=20\Rightarrow \Delta f = 1/T = 0.05 Hz bin spacing. (1) Nyquist: np.fft/rfftfreq show max resolvable freq =1/(2Δt)=1/0.1=10=1/(2\Delta t)=1/0.1=10 Hz. (2)


Question 3 (18)

(a) (4) The model is nonlinear in λ\lambda; the sum-of-squares surface has flat/curved valleys, so LM can stall or diverge from a poor start. Principled guesses: C0=minyC_0=\min y (or last-time plateau); A0=y(t0)C0A_0=y(t_0)-C_0; λ0\lambda_0 from a log-linear fit: ln(yiC0)lnA0λti\ln(y_i-C_0)\approx \ln A_0 -\lambda t_i, take slope. (4)

(b) (4) pcov is the estimated covariance matrix of popt. Standard error on λ\lambda: 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 JJ)1J^\top J)^{-1}. (4)

(c) (6) Normal equations solve AAx=AbA^\top A x = A^\top b. Via SVD A=UΣVA=U\Sigma V^\top, AA=VΣ2VA^\top A = V\Sigma^2 V^\top, so singular values get squared: κ(AA)=κ(A)2\kappa(A^\top A)=\kappa(A)^2. (3) Squaring the condition number squares the error amplification; small singular values become negligible, losing accuracy (and AAA^\top A may become numerically singular). QR/lstsq (SVD-based) work directly on AA, keeping conditioning at κ(A)\kappa(A), 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".) H0H_0: the two samples are drawn from the same distribution. p =0.002<α=0.05=0.002<\alpha=0.05\Rightarrow reject H0H_0: 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-