Course
Python Lab Reference

Python Lab Reference

Every library, method, and pattern you need for the Numerical Analysis labs.

74 entries across 8 libraries

🔢NumPy · Arrays

Numerical Analysis lives on arrays and matrices. These build the grids, vectors and coefficient matrices every algorithm operates on.

import numpy as np

Building arrays & grids

np.array→ ndarray
np.array(object)

Turn a Python list (or list of lists) into a vector or matrix.

v = np.array([1.0, 2.0, 3.0])
A = np.array([[2., 1.], [1., 3.]])   # a 2×2 matrix
Use floats so integer division never silently truncates your results.
np.linspace→ ndarray
np.linspace(start, stop, num)

num evenly spaced points including both endpoints — your standard sample grid.

x = np.linspace(0, 1, 101)   # 0.00, 0.01, … 1.00
Includes the endpoint. For a step size h over [a,b], use num = (b-a)/h + 1.
np.arange→ ndarray
np.arange(start, stop, step)

Values from start up to (but excluding) stop, spaced by step.

k = np.arange(0, 10)      # 0..9
h = np.arange(0.1, 1.0, 0.1)
stop is EXCLUDED (unlike linspace). Float steps can miss the last point by rounding.
np.logspace→ ndarray
np.logspace(a, b, num)

num points spaced evenly in log10 — perfect for step-size / error sweeps.

hs = np.logspace(-1, -6, 20)   # 1e-1 down to 1e-6
np.zeros / np.ones→ ndarray
np.zeros(shape) · np.ones(shape)

Pre-allocate an array of zeros or ones. shape can be n or (rows, cols).

x = np.zeros(n)          # solution vector
A = np.zeros((n, n))     # coefficient matrix to fill in
np.zeros_like / ones_like / full_like→ ndarray

Same shape & dtype as an existing array — great inside loops that build results.

errs = np.zeros_like(hs)   # one slot per step size
np.eye→ ndarray
np.eye(n)

The n×n identity matrix.

I = np.eye(3)
np.diag→ ndarray
np.diag(v) / np.diag(A)

Make a diagonal matrix from a vector — or extract the diagonal of a matrix.

D = np.diag([1., 2., 3.])   # vector → diagonal matrix
d = np.diag(A)              # matrix → its diagonal
Its meaning flips based on the input shape: 1-D in ⇒ matrix out, 2-D in ⇒ vector out.
np.vander→ ndarray
np.vander(x, N)

Vandermonde matrix — the design matrix for polynomial fitting/interpolation.

V = np.vander(x, len(x))   # solve V c = y for polynomial coeffs
Vandermonde matrices are notoriously ill-conditioned — watch np.linalg.cond.
np.tril / np.triu→ ndarray

Lower / upper triangular part of a matrix (rest zeroed) — for LU-style work.

L = np.tril(A, -1) + np.eye(n)   # unit lower-triangular
np.vstack→ ndarray

Stack arrays as rows into a taller matrix.

M = np.vstack([row1, row2, row3])

➗NumPy · Math & Reductions

Elementwise math and the reductions you use to measure error and convergence. Every function here works on a whole array at once — vectorize instead of looping.

import numpy as np

Elementwise functions

np.abs→ float or ndarray

Absolute value — the backbone of every error measurement.

err = np.abs(approx - exact)
np.exp / np.log

e^x and natural log (ln). np.log10 for base 10.

f = np.exp(-x**2)
rate = np.log(err[1:] / err[:-1])
np.log is base-e. Convergence-order plots usually want np.log10 or a loglog axis.
np.sqrt

Square root, elementwise.

rms = np.sqrt(np.sum(r**2) / n)
np.sin / np.cos

Trig functions (radians) — common test integrands and ODE right-hand sides.

y = np.sin(np.pi * x)
np.pi · np.e

The constants π and e. No parentheses — they are values, not functions.

area = np.pi * r**2
np.round
np.round(x, decimals)

Round to a number of decimal places (display / comparison).

print(np.round(A, 4))
np.clip
np.clip(x, lo, hi)

Clamp values into [lo, hi] — guards against out-of-range arguments.

theta = np.arccos(np.clip(c, -1, 1))

Reductions & comparisons

np.max / np.min→ float

Largest / smallest element — max is how you report worst-case error.

max_err = np.max(np.abs(u - u_exact))
np.argmin / np.argmax→ int

INDEX of the smallest / largest element (not the value).

i = np.argmin(errors)   # which step size was best
np.sum→ float or ndarray
np.sum(a, axis=None)

Sum of elements — quadrature rules, norms, residuals.

integral = h * np.sum(f)   # basic Riemann/trapezoid core
np.diff→ ndarray (one shorter than input)

Consecutive differences — finite-difference derivatives and spacing checks.

slopes = np.diff(y) / np.diff(x)
np.allclose / np.isclose→ bool (allclose) / bool array (isclose)
np.allclose(a, b, rtol=1e-5, atol=1e-8)

Are arrays equal within tolerance? Use this, never == on floats.

assert np.allclose(A @ x, b)   # did the solver actually solve it?
Floating-point results are almost never bit-exact — always compare with a tolerance.
np.sort / np.searchsorted

Sort values / find the insertion index in a sorted array (piecewise lookup).

i = np.searchsorted(nodes, x)   # which interval x falls in
np.interp→ float or ndarray
np.interp(x, xp, fp)

Fast piecewise-linear interpolation at points x from samples (xp, fp).

y = np.interp(0.37, xp, fp)
Linear only. For smooth cubic interpolation use scipy CubicSpline.

🧮NumPy · Linear Algebra

The np.linalg module powers the linear-systems half of the course: solving Ax=b, conditioning, eigenvalues and least squares.

import numpy as np

Solving & inverting

np.linalg.solve→ ndarray (solution x)
np.linalg.solve(A, b)

Solve the linear system A x = b directly. The right way to “apply A⁻¹”.

x = np.linalg.solve(A, b)
Prefer this over inv(A) @ b — it is faster and numerically more accurate.
np.linalg.inv→ ndarray
np.linalg.inv(A)

The matrix inverse A⁻¹.

Ainv = np.linalg.inv(A)
Rarely what you actually want — to solve a system, use solve(). Inverting amplifies error.
np.linalg.norm→ float
np.linalg.norm(x, ord=None)

Vector or matrix norm — measure residuals and error magnitudes.

residual = np.linalg.norm(A @ x - b)
rel_err  = np.linalg.norm(x - x_true) / np.linalg.norm(x_true)
ord=np.inf gives the max-abs (∞) norm; ord=1 the sum-abs norm.

Conditioning & spectra

np.linalg.cond→ float
np.linalg.cond(A)

Condition number κ(A) — how much A amplifies input error. The course’s key diagnostic.

print(f"cond = {np.linalg.cond(A):.2e}")
Large κ ⇒ ill-conditioned: tiny data changes cause big solution changes. κ ≈ 10^k loses ~k digits.
np.linalg.eigvals→ ndarray (possibly complex)
np.linalg.eigvals(A)

Eigenvalues of A (values only). Use eig() if you also need eigenvectors.

lam = np.linalg.eigvals(A)
spectral_radius = np.max(np.abs(lam))
Spectral radius < 1 is the convergence test for iterative methods (Jacobi/Gauss–Seidel).

Factorizations & least squares

np.linalg.lstsq→ (solution, residuals, rank, singular_values)
np.linalg.lstsq(A, b, rcond=None)

Least-squares solution to an overdetermined system (best fit).

coef, residuals, rank, sv = np.linalg.lstsq(A, b, rcond=None)
Returns a tuple — unpack four values even if you only want the first.
np.linalg.cholesky→ ndarray (lower triangular)
np.linalg.cholesky(A)

Cholesky factor L (A = L Lᵀ) for symmetric positive-definite matrices.

L = np.linalg.cholesky(A)
Raises LinAlgError if A is not positive-definite — a useful SPD check in itself.
np.linalg.qr→ (Q, R)
np.linalg.qr(A)

QR factorization A = Q R (orthogonal × upper-triangular).

Q, R = np.linalg.qr(A)

🎲Floating Point & Random

Numerical Analysis starts with how computers store numbers. These expose machine precision and generate test data.

import numpy as np

Machine precision

np.finfo→ float
np.finfo(float).eps

Machine epsilon — the gap between 1.0 and the next float (≈ 2.22e-16 for float64).

eps = np.finfo(float).eps
print(f"machine epsilon = {eps:.2e}")
The fundamental limit on relative accuracy. No float64 result is meaningful past ~16 digits.
np.inf

Positive infinity — sentinel for “no bound yet” or a diverged value.

best = np.inf   # then keep the minimum found

Random test data

np.random.seed
np.random.seed(0)

Fix the random seed so results are reproducible (and match the grader).

np.random.seed(42)
Set this BEFORE generating any random data if you want repeatable output.
np.random.rand→ ndarray
np.random.rand(d0, d1, …)

Uniform random values in [0, 1) with the given shape.

A = np.random.rand(n, n)
np.random.randn→ ndarray
np.random.randn(d0, d1, …)

Standard-normal (mean 0, std 1) random values — noise for perturbation tests.

noise = 1e-6 * np.random.randn(n)

📈Matplotlib

Every convergence study ends in a plot. The labs use the object-oriented style: make a figure and axes with plt.subplots, then draw on the ax. Log axes reveal convergence order.

import matplotlib.pyplot as plt

Figure & axes

plt.subplots→ (Figure, Axes)
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(w, h))

Create a figure and one (or a grid of) axes. The standard opening line.

fig, ax = plt.subplots(figsize=(7, 4))
fig, (ax1, ax2) = plt.subplots(1, 2)   # two side-by-side
plt.tight_layout

Auto-adjust spacing so labels/titles don’t overlap or clip.

plt.tight_layout()
Call it just before savefig/show.
plt.savefig
plt.savefig('out.png', dpi=150)

Write the figure to a file — this is how the lab captures your plot.

plt.savefig('convergence.png', dpi=150, bbox_inches='tight')
plt.show

Render the figure. In the lab runner, pair with savefig so output is captured.

plt.show()

Drawing on an axis

ax.plot
ax.plot(x, y, 'o-', label='exact')

Line/marker plot. Third arg is a style string; label feeds the legend.

ax.plot(x, y, 'b.-', label='approx')
ax.semilogy
ax.semilogy(x, err)

Linear x, log y — shows exponential decay of error as a straight line.

ax.semilogy(iterations, residuals)
Use for “error vs iteration”: geometric convergence looks linear here.
ax.loglog
ax.loglog(h, err)

Log-log plot — the slope IS the order of accuracy p (err ∝ hᵖ).

ax.loglog(hs, errors, "o-")
A slope of 2 means second-order. This is the money plot of the course.
ax.set_xlabel / set_ylabel / set_title

Label the axes and title the plot.

ax.set_xlabel('step size h'); ax.set_ylabel('error'); ax.set_title('Convergence')
ax.legend

Show the legend built from each plot’s label=.

ax.legend()
Does nothing unless your plot calls passed label=.
ax.grid

Add grid lines — helpful for reading slopes off log plots.

ax.grid(True, which='both', alpha=0.3)
ax.axhline
ax.axhline(y, ...)

Draw a horizontal reference line (e.g. a tolerance or exact value).

ax.axhline(eps, color='r', ls='--', label='machine eps')
ax.set_ylim

Fix the y-axis range so tiny errors don’t collapse to nothing.

ax.set_ylim(1e-16, 1)

🔬SciPy

Battle-tested implementations to check your own code against. The labs use interpolation, root-finding, integration and LU factorization from SciPy as reference answers.

from scipy.interpolate import CubicSpline
from scipy.optimize import brentq
from scipy.integrate import quad, solve_ivp
from scipy.linalg import lu_factor, lu_solve, hilbert

Interpolation

CubicSpline→ a callable spline object
cs = CubicSpline(x, y)

Build a smooth natural cubic spline through the data points, then call it like a function.

cs = CubicSpline(x, y)
y_fine = cs(x_fine)          # evaluate anywhere
d1 = cs(x_fine, 1)           # first derivative
cs(x, 1) / cs(x, 2) give derivatives; cs.integrate(a, b) integrates the spline.

Root finding

brentq→ float (the root)
brentq(f, a, b)

Robustly find a root of f in the bracket [a, b] where f changes sign.

root = brentq(lambda x: x**2 - 2, 0, 2)   # → √2
Requires f(a) and f(b) to have opposite signs — otherwise it raises. Great gold-standard to test bisection/Newton against.

Integration

quad→ (value, estimated_error)
val, err = quad(f, a, b)

Adaptive high-accuracy definite integral — the reference “exact” value for quadrature labs.

val, err = quad(lambda x: np.exp(-x**2), 0, 1)
Returns a tuple; the second element is an error estimate, not the answer.
solve_ivp→ solution object with .t and .y
solve_ivp(fun, t_span, y0, t_eval=None, rtol=1e-3)

Integrate an ODE system dy/dt = fun(t, y) — the reference for your Euler/RK code.

def f(t, y):
    return -y                      # y' = -y
sol = solve_ivp(f, [0, 5], [1.0], t_eval=np.linspace(0, 5, 100))
y = sol.y[0]                       # solution over time
sol.y is shaped (n_equations, n_times) — each row is one variable through time.

LU factorization (scipy.linalg)

lu_factor / lu_solve→ lu_factor → (lu, piv); lu_solve → solution x
lu, piv = lu_factor(A); x = lu_solve((lu, piv), b)

Factor A once, then solve for many right-hand sides cheaply.

lu, piv = lu_factor(A)
x1 = lu_solve((lu, piv), b1)
x2 = lu_solve((lu, piv), b2)   # reuse the factorization
The whole point: factor is the expensive step (O(n³)); each solve is cheap (O(n²)).
hilbert→ ndarray
hilbert(n)

The n×n Hilbert matrix — the classic ill-conditioned test case.

H = hilbert(6)
print(np.linalg.cond(H))   # enormous
Used to demonstrate how conditioning wrecks naive solvers.

🐍Python Essentials

The plain-Python and math-module pieces that show up across the labs — clean numeric output, functions, loops, and a couple of special helpers.

Formatting numbers

:.6f (fixed decimals)

Show a set number of decimal places — for tables of approximations.

print(f"x = {x:.6f}")
:.3e (scientific)

Scientific notation — the natural format for errors and condition numbers.

print(f"error = {err:.3e},  cond = {kappa:.2e}")
:>12 (alignment)

Pad to a width for aligned columns in convergence tables.

print(f"{n:>4}{err:>14.3e}{ratio:>10.2f}")
Perfect for the “n | error | ratio” tables the labs ask you to print.

Functions, loops & math

def
def f(x):

Define a function. 4-space indented body; remember to return.

def newton(f, df, x0, tol=1e-10):
    x = x0
    while abs(f(x)) > tol:
        x = x - f(x) / df(x)
    return x
lambda

A one-line anonymous function — handy for passing f to a solver.

root = brentq(lambda x: x**3 - x - 2, 1, 2)
for / while

Iterate a fixed number of times, or until a tolerance is met.

for k in range(max_iter):
    x_new = g(x)
    if abs(x_new - x) < tol:
        break
    x = x_new
math.ceil / math.log
import math

Scalar math helpers — e.g. estimating how many iterations bisection needs.

n_steps = math.ceil(math.log((b - a) / tol, 2))
For arrays use np.ceil / np.log; math.* is for single numbers.
np.polynomial.legendre.leggauss→ (nodes, weights)
nodes, weights = np.polynomial.legendre.leggauss(n)

Gauss–Legendre quadrature nodes & weights on [-1, 1].

x, w = np.polynomial.legendre.leggauss(5)
# map [-1,1] → [a,b] then integral ≈ np.sum(w * f(xm)) * (b-a)/2
Nodes are on [-1, 1]; rescale to your interval before summing.

📖NA Cookbook

The recurring ideas and traps behind the labs. If a number looks wrong, the explanation is almost always here.

Go-to recipes

Relative vs absolute error

Absolute error is |approx − exact|; relative divides by |exact| (scale-free).

abs_err = np.abs(approx - exact)
rel_err = abs_err / np.abs(exact)
Report relative error when magnitudes vary; guard against exact ≈ 0.
Order of accuracy from a log-log slope

If error ∝ hᵖ, then log(err) vs log(h) is a line of slope p.

p = np.polyfit(np.log(hs), np.log(errs), 1)[0]
print(f"observed order ≈ {p:.2f}")
The empirical way to confirm a method is 1st, 2nd, 4th-order, etc.
Error ratio table

Halving h and watching the error ratio reveals the order (ratio ≈ 2ᵖ).

ratio = errs[:-1] / errs[1:]   # ~2 ⇒ 1st order, ~4 ⇒ 2nd order
Check your linear solve

Always verify the residual after solving Ax=b.

assert np.linalg.norm(A @ x - b) < 1e-8

Common traps

Never compare floats with ==

Rounding makes exact equality almost always False.

# WRONG: if x == 0.3
if np.isclose(x, 0.3): ...
Use np.isclose / np.allclose with a tolerance.
Catastrophic cancellation

Subtracting two nearly equal numbers destroys significant digits.

# unstable:  (1 - np.cos(x)) / x**2   for small x
# stable:    0.5 * (np.sinc(x/np.pi))**2  or a Taylor form
Symptom: accuracy collapses as x→0. Rearrange the formula to avoid the subtraction.
inv(A) @ b vs solve(A, b)

Forming the inverse is slower and less accurate than solving directly.

# avoid: x = np.linalg.inv(A) @ b
x = np.linalg.solve(A, b)
Ill-conditioning (large κ)

A big condition number means the answer is untrustworthy no matter the algorithm.

if np.linalg.cond(A) > 1e12:
    print("warning: nearly singular")
κ ≈ 10^k ⇒ expect to lose about k digits of accuracy. Hilbert/Vandermonde are classic offenders.
arange endpoint / float steps

np.arange excludes the stop and can drop the last point due to rounding.

# safer for a known count:
x = np.linspace(a, b, num)
Use linspace when you need an exact endpoint or an exact number of points.
Loop instead of vectorize

Python loops over arrays are slow and error-prone — operate on whole arrays.

# slow: for i in range(n): y[i] = f(x[i])
y = f(x)   # if f uses np.* it already vectorizes