← Math|88 of 100
Numerical Methods

Numerical Integration

Master trapezoidal, Simpson's, Gaussian quadrature, and Monte Carlo integration.

📂 Integration📖 Lesson 88 of 100🎓 Free Course

Advertisement

Numerical Integration

ℹ️ Why It Matters

Many definite integrals cannot be evaluated in closed form — the antiderivative may not exist as elementary functions, the integrand may only be known at discrete data points, or the integral may be over a high-dimensional domain. Numerical integration (quadrature) provides approximate values with controlled accuracy, forming the backbone of scientific computing, statistical inference, and physics simulations.

💡 Real-World Impact

Monte Carlo integration is the only feasible method for high-dimensional integrals in physics (path integrals), finance (option pricing), and machine learning (variational inference). Adaptive quadrature computes precise values for engineering simulations. Every numerical ODE solver uses quadrature internally.


Definitions

DfNumerical Integration (Quadrature)

Numerical integration approximates the value of a definite integral ∫ₐᵇ f(x)dx using a weighted sum of function values at specific points: ∫ₐᵇ f(x)dx ≈ Σ wᵢf(xᵢ), where wᵢ are weights and xᵢ are quadrature nodes.

DfComposite Rule

A composite rule divides [a, b] into n subintervals of width h = (b−a)/n and applies the basic rule to each subinterval, summing the results. This reduces error by increasing n without requiring high-degree polynomial approximation over the entire interval.

DfDegree of Exactness

A quadrature rule has degree of exactness d if it integrates all polynomials of degree ≤ d exactly. A rule with n nodes can have degree of exactness at most 2n − 1 (Gaussian quadrature achieves this).

DfAdaptive Quadrature

Adaptive quadrature automatically subdivides intervals where the integrand varies rapidly, concentrating function evaluations where they contribute most to accuracy. This achieves a prescribed error tolerance with fewer evaluations than uniform subdivision.

DfMonte Carlo Integration

Monte Carlo integration estimates ∫f(x)dx by computing the average of f evaluated at random sample points, multiplied by the domain volume: ∫f(x)dx ≈ V·(1/N)Σf(xᵢ). The error decreases as O(1/√N), independent of dimension.


Formulas

Trapezoidal Rule (Single Interval)

abf(x)dxba2[f(a)+f(b)]\int_a^b f(x)\,dx \approx \frac{b-a}{2}[f(a) + f(b)]

Here,

  • a,ba, b=Integration limits
  • f(a),f(b)f(a), f(b)=Function values at endpoints

Composite Trapezoidal Rule

abf(x)dxh2[f(x0)+2i=1n1f(xi)+f(xn)]\int_a^b f(x)\,dx \approx \frac{h}{2}\left[f(x_0) + 2\sum_{i=1}^{n-1}f(x_i) + f(x_n)\right]

Here,

  • hh=Step size: h = (b−a)/n
  • nn=Number of subintervals
  • xix_i=Node: x_i = a + ih

Trapezoidal Error

ET=(ba)h212f(ξ),ξ[a,b]E_T = -\frac{(b-a)h^2}{12}f''(\xi), \quad \xi \in [a,b]

Here,

  • f(ξ)f''(ξ)=Second derivative evaluated at some point ξ in [a,b]

Simpson's Rule (Composite)

abf(x)dxh3[f(x0)+4i oddf(xi)+2i evenf(xi)+f(xn)]\int_a^b f(x)\,dx \approx \frac{h}{3}\left[f(x_0) + 4\sum_{i \text{ odd}} f(x_i) + 2\sum_{i \text{ even}} f(x_i) + f(x_n)\right]

Here,

  • hh=Step size: h = (b−a)/n, n must be even
  • nn=Number of subintervals (must be even)

Simpson's Error

ES=(ba)h4180f(4)(ξ),ξ[a,b]E_S = -\frac{(b-a)h^4}{180}f^{(4)}(\xi), \quad \xi \in [a,b]

Here,

  • f(4)(ξ)f^(4)(ξ)=Fourth derivative evaluated at some point ξ in [a,b]

Gaussian Quadrature

11f(x)dxi=1nwif(xi)\int_{-1}^{1} f(x)\,dx \approx \sum_{i=1}^{n} w_i f(x_i)

Here,

  • xix_i=Roots of the nth Legendre polynomial P_n(x)
  • wiw_i=Corresponding weights

Monte Carlo Integration

Ωf(x)dxV(Ω)1Ni=1Nf(xi)\int_\Omega f(x)\,dx \approx V(\Omega) \cdot \frac{1}{N}\sum_{i=1}^{N} f(x_i)

Here,

  • V(Ω)V(Ω)=Volume of integration domain
  • NN=Number of random samples
  • xix_i=Random sample drawn uniformly from Ω

Monte Carlo Error

SE=σN,σ2=1N(f(xi)fˉ)2\text{SE} = \frac{\sigma}{\sqrt{N}}, \quad \sigma^2 = \frac{1}{N}\sum(f(x_i) - \bar{f})^2

Here,

  • σσ=Standard deviation of f values
  • NN=Number of samples

Trapezoidal Rule

ℹ️ Geometric Interpretation

The trapezoidal rule approximates the area under the curve by connecting adjacent points with straight lines, forming trapezoids. The area of each trapezoid is h·(f(xᵢ) + f(xᵢ₊₁))/2. This is the simplest numerical integration rule with O(h²) accuracy.

Derivation

From the single-interval trapezoidal rule applied to each subinterval and summed:

  • Total = Σᵢ₌₀ⁿ⁻¹ h·(f(xᵢ) + f(xᵢ₊₁))/2
  • Interior points appear twice (once as right endpoint, once as left)
  • Endpoints appear once
  • Result: h/2·[f(x₀) + 2f(x₁) + ... + 2f(x₋₁) + f(xₙ)]

Simpson's Rule

ℹ️ Geometric Interpretation

Simpson's rule approximates the integrand by parabolas. Each pair of adjacent subintervals is fit with a quadratic polynomial through three points. The area under the parabola gives the 1-3-1 weight pattern (scaled by h/3). This achieves O(h⁴) accuracy — far superior to the trapezoidal rule for the same number of evaluations.

Weight Pattern

For n = 6 (7 nodes): weights are [1, 4, 2, 4, 2, 4, 1] × h/3 Pattern: 1, 4, 2, 4, 2, 4, ..., 4, 1


Gaussian Quadrature

ℹ️ Optimal Node Placement

Instead of equally spaced nodes, Gaussian quadrature places nodes at the roots of Legendre polynomials and chooses optimal weights. This achieves degree of exactness 2n−1 with n nodes — the maximum possible for any n-point rule. For example, 3-point Gaussian quadrature integrates polynomials up to degree 5 exactly.

Legendre Polynomial Nodes and Weights

nNodes (xᵢ)Weights (wᵢ)Exact for degree
1021
2±1/√313
30, ±√(3/5)8/9, 5/95
4±0.339981, ±0.8611360.652145, 0.3478557

Transformation

To integrate over [a, b] instead of [−1, 1]: x = ((b−a)t + (b+a))/2, dx = (b−a)/2 dt


Examples

📝Comparing Integration Methods on e^x

Compute ∫₀¹ eˣ dx, whose exact value is e − 1 ≈ 1.71828. Compare trapezoidal, Simpson's, and Gaussian quadrature with varying numbers of nodes.

💡Implementation: Method Comparison

import numpy as np


def trapezoidal(f, a, b, n=1000):
    """Composite trapezoidal rule."""
    x = np.linspace(a, b, n + 1)
    y = f(x)
    h = (b - a) / n
    return h * (y[0] / 2 + np.sum(y[1:-1]) + y[-1] / 2)


def simpson(f, a, b, n=1000):
    """Composite Simpson's rule (n must be even)."""
    if n % 2 != 0:
        n += 1
    x = np.linspace(a, b, n + 1)
    y = f(x)
    h = (b - a) / n
    return h / 3 * (y[0] + 4 * np.sum(y[1::2]) + 2 * np.sum(y[2:-1:2]) + y[-1])


def gauss_legendre(f, a, b, n=5):
    """Gaussian quadrature using numpy's Gauss-Legendre nodes."""
    nodes, weights = np.polynomial.legendre.leggauss(n)
    # Transform from [-1, 1] to [a, b]
    t = 0.5 * (b - a) * nodes + 0.5 * (b + a)
    w = 0.5 * (b - a) * weights
    return np.sum(w * f(t))


# Test function
f = np.exp
exact = np.e - 1

print("Comparing Integration Methods for ∫₀¹ eˣ dx")
print(f"Exact value: {exact:.12f}")
print(f"{'Method':<20} {'n':<6} {'Result':<16} {'Error':<12}")
print("-" * 56)

for n in [4, 8, 16, 32, 64]:
    trap = trapezoidal(f, 0, 1, n)
    simp = simpson(f, 0, 1, n)
    gauss = gauss_legendre(f, 0, 1, n // 2 + 1)

    print(f"{'Trapezoidal':<20} {n:<6} {trap:<16.12f} {abs(trap - exact):<12.2e}")
    print(f"{'Simpson':<20} {n:<6} {simp:<16.12f} {abs(simp - exact):<12.2e}")
    print(f"{'Gauss-Legendre':<20} {n:<6} {gauss:<16.12f} {abs(gauss - exact):<12.2e}")
    print()

📝Adaptive Quadrature with scipy

scipy.integrate.quad uses adaptive Gaussian quadrature (QUADPACK library) that automatically adjusts the subdivision based on the integrand's behavior.

💡Adaptive Quadrature Implementation

import numpy as np
from scipy.integrate import quad


def integrand(x):
    """A function with a sharp peak — challenges uniform methods."""
    return np.exp(-x**2) * np.sin(10 * x) ** 2


# Adaptive quadrature
result, error = quad(integrand, -5, 5)
print(f"Adaptive quadrature: {result:.12f}")
print(f"Estimated error:     {error:.2e}")
print(f"Actual evaluations:  likely ~50-200 (adaptive)")

# Compare with fixed methods
print(f"\nFixed n=100:")
from numpy import sin, exp

def trapezoidal(f, a, b, n):
    x = np.linspace(a, b, n + 1)
    y = f(x)
    h = (b - a) / n
    return h * (y[0]/2 + np.sum(y[1:-1]) + y[-1]/2)

trap_100 = trapezoidal(integrand, -5, 5, 100)
print(f"  Trapezoidal (n=100): {trap_100:.12f}")
print(f"  Error:               {abs(trap_100 - result):.2e}")

# Oscillatory integral
def oscillatory(x):
    return np.sin(100 * x) / (1 + x**2)

result_osc, error_osc = quad(oscillatory, 0, 10)
print(f"\nOscillatory integral:  {result_osc:.8f}")
print(f"Estimated error:      {error_osc:.2e}")

📝Monte Carlo Integration in 2D

Estimate the area of a unit circle using random sampling — a classic Monte Carlo integration problem.

💡Monte Carlo Circle Area

import numpy as np


def monte_carlo_circle(n_samples):
    """Estimate π using Monte Carlo integration."""
    x = np.random.uniform(-1, 1, n_samples)
    y = np.random.uniform(-1, 1, n_samples)

    # Count points inside the unit circle
    inside = x**2 + y**2 <= 1.0
    pi_estimate = 4.0 * np.sum(inside) / n_samples

    return pi_estimate


# Demonstrate convergence
print("Monte Carlo Estimation of π")
print(f"{'Samples':<12} {'Estimate':<14} {'Error':<12}")
print("-" * 38)

for n in [100, 1_000, 10_000, 100_000, 1_000_000]:
    np.random.seed(42)
    est = monte_carlo_circle(n)
    print(f"{n:<12,} {est:<14.8f} {abs(est - np.pi):<12.6f}")

# Verify O(1/√N) convergence
print(f"\nExpected error reduction for 10x samples: {1/np.sqrt(10):.4f}")

📝Integration of Discrete Data

When given data points rather than a function, use the trapezoidal rule on the data or fit an interpolant first.

💡Integrating Experimental Data

import numpy as np


# Simulated experimental data: temperature measurements over time
time = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])  # seconds
temp = np.array([20, 22, 25, 28, 31, 33, 34, 34, 33, 31, 28])  # °C

# Trapezoidal rule on discrete data
dt = time[1] - time[0]  # Uniform spacing
area_trap = dt * (temp[0]/2 + np.sum(temp[1:-1]) + temp[-1]/2)
print(f"Trapezoidal (manual):  {area_trap:.2f} °C·s")

# Using numpy
area_np = np.trapz(temp, time)
print(f"numpy.trapz:           {area_np:.2f} °C·s")

# Simpson's rule (n must be even — we have 11 points, 10 intervals)
n = len(time) - 1  # 10 intervals
h = (time[-1] - time[0]) / n
area_simp = h/3 * (temp[0] + 4*np.sum(temp[1::2]) + 2*np.sum(temp[2:-1:2]) + temp[-1])
print(f"Simpson's rule:        {area_simp:.2f} °C·s")

# Physical interpretation
print(f"\nMean temperature: {area_trap / (time[-1] - time[0]):.2f} °C")

Theorems

ThTrapezoidal Rule Error Bound

If f is twice continuously differentiable on [a, b], then the error of the composite trapezoidal rule satisfies: |E_T| ≤ (b−a)h²/12 · max|f''(x)| for x ∈ [a,b] where h = (b−a)/n is the step size.

ThSimpson's Rule Error Bound

If f is four times continuously differentiable on [a, b], then the error of the composite Simpson's rule satisfies: |E_S| ≤ (b−a)h⁴/180 · max|f⁽⁴⁾(x)| for x ∈ [a,b] This is O(h⁴), much more accurate than the trapezoidal rule's O(h²) for the same h.

ThGaussian Quadrature Exactness

The n-point Gaussian quadrature rule with Legendre polynomials integrates all polynomials of degree ≤ 2n−1 exactly. No other n-point rule can achieve higher degree of exactness.

ThMonte Carlo Convergence

For integrable f with variance σ², the Monte Carlo estimate I̅_N = V·(1/N)Σf(xᵢ) converges to the true integral I at rate O(1/√N), with: P(|I̅_N − I| > ε) ≤ σ²/(Nε²) by Chebyshev's inequality. The convergence rate is independent of dimension.

ThRichardson Extrapolation

If a numerical method has an error expansion E = c₁h^p + c₂h^(p+1) + ..., then combining results at step sizes h and h/2 can eliminate the leading error term, doubling the order of accuracy. This is the foundation of Romberg integration.


Python Implementation

import numpy as np
from typing import Callable, Tuple


class NumericalIntegrator:
    """Comprehensive numerical integration toolkit."""

    @staticmethod
    def trapezoidal(f: Callable, a: float, b: float,
                    n: int = 1000) -> Tuple[float, float]:
        """Composite trapezoidal rule. Returns (result, error_estimate)."""
        x = np.linspace(a, b, n + 1)
        y = f(x)
        h = (b - a) / n
        result = h * (y[0] / 2 + np.sum(y[1:-1]) + y[-1] / 2)

        # Error estimate via Richardson extrapolation
        x2 = np.linspace(a, b, 2 * n + 1)
        y2 = f(x2)
        h2 = h / 2
        result2 = h2 * (y2[0] / 2 + np.sum(y2[1:-1]) + y2[-1] / 2)

        error = abs(result2 - result) / 3  # Richardson estimate
        return result, error

    @staticmethod
    def simpson(f: Callable, a: float, b: float,
                n: int = 1000) -> Tuple[float, float]:
        """Composite Simpson's rule (n forced even). Returns (result, error)."""
        if n % 2 != 0:
            n += 1
        x = np.linspace(a, b, n + 1)
        y = f(x)
        h = (b - a) / n
        result = h / 3 * (y[0] + 4 * np.sum(y[1::2]) +
                          2 * np.sum(y[2:-1:2]) + y[-1])

        # Error estimate
        n2 = 2 * n
        x2 = np.linspace(a, b, n2 + 1)
        y2 = f(x2)
        h2 = (b - a) / n2
        result2 = h2 / 3 * (y2[0] + 4 * np.sum(y2[1::2]) +
                            2 * np.sum(y2[2:-1:2]) + y2[-1])

        error = abs(result2 - result) / 15  # Richardson estimate
        return result, error

    @staticmethod
    def gauss_legendre(f: Callable, a: float, b: float,
                       n: int = 5) -> float:
        """n-point Gauss-Legendre quadrature."""
        nodes, weights = np.polynomial.legendre.leggauss(n)
        t = 0.5 * (b - a) * nodes + 0.5 * (b + a)
        w = 0.5 * (b - a) * weights
        return np.sum(w * f(t))

    @staticmethod
    def romberg(f: Callable, a: float, b: float,
                max_level: int = 10) -> np.ndarray:
        """Romberg integration using Richardson extrapolation."""
        R = np.zeros((max_level + 1, max_level + 1))

        # Trapezoidal estimates at various levels
        n = 1
        h = b - a
        R[0, 0] = h / 2 * (f(a) + f(b))

        for i in range(1, max_level + 1):
            n = 2 ** i
            h = (b - a) / n
            # Add new midpoints
            x_new = np.arange(1, n, 2) * h + a
            R[i, 0] = 0.5 * R[i-1, 0] + h * np.sum(f(x_new))

            # Richardson extrapolation
            for j in range(1, i + 1):
                R[i, j] = R[i, j-1] + (R[i, j-1] - R[i-1, j-1]) / (4**j - 1)

        return R

    @staticmethod
    def monte_carlo(f: Callable, domain: list,
                    n_samples: int = 100000) -> Tuple[float, float]:
        """Monte Carlo integration over a rectangular domain."""
        a, b = domain[0], domain[1]
        volume = b - a

        x = np.random.uniform(a, b, n_samples)
        values = f(x)
        result = volume * np.mean(values)
        std_err = volume * np.std(values) / np.sqrt(n_samples)

        return result, std_err


def demonstrate_integration():
    """Full demonstration of all integration methods."""
    print("=" * 60)
    print("NUMERICAL INTEGRATION METHOD COMPARISON")
    print("=" * 60)

    # Test 1: Smooth function
    f1 = lambda x: np.exp(-x**2)
    exact1 = np.sqrt(np.pi)  # ∫₋∞^∞ e^(-x²) dx = √π
    # But we integrate from -5 to 5 (close enough)

    from scipy.integrate import quad
    exact1_quad, _ = quad(f1, -5, 5)

    print(f"\n∫₋₅⁵ e^(-x²) dx")
    print(f"scipy quad: {exact1_quad:.12f}")

    integrator = NumericalIntegrator()

    for n in [10, 50, 200]:
        trap, err = integrator.trapezoidal(f1, -5, 5, n)
        simp, err = integrator.simpson(f1, -5, 5, n)
        print(f"  n={n:>3}: Trapezoidal {trap:.10f} (err {err:.2e}) | "
              f"Simpson {simp:.10f} (err {err:.2e})")

    # Gauss-Legendre
    for n in [3, 5, 10]:
        gl = integrator.gauss_legendre(f1, -5, 5, n)
        print(f"  Gauss-Legendre (n={n}): {gl:.10f}")

    # Romberg
    R = integrator.romberg(f1, -5, 5, max_level=6)
    print(f"\nRomberg integration:")
    for i in range(min(7, R.shape[0])):
        print(f"  Level {i}: {R[i, i]:.12f}")

    # Test 2: Oscillatory function
    print(f"\n{'=' * 60}")
    f2 = lambda x: np.sin(100 * x) / (1 + x**2)
    mc_result, mc_err = integrator.monte_carlo(f2, [-10, 10], 1_000_000)
    quad_result, _ = quad(f2, -10, 10)
    print(f"∫₋₁₀¹⁰ sin(100x)/(1+x²) dx")
    print(f"scipy quad:  {quad_result:.10f}")
    print(f"Monte Carlo: {mc_result:.10f} (±{mc_err:.2e})")


demonstrate_integration()

Applications in AI/ML

ℹ️ Integration in Machine Learning

Numerical integration appears throughout ML: variational inference approximates intractable posteriors via quadrature, Bayesian models compute evidence (marginal likelihood) by integrating over parameters, and reinforcement learning estimates expected returns via Monte Carlo sampling.

Variational Inference

In Bayesian neural networks, the posterior P(θ|D) is intractable. Variational inference approximates it with a tractable distribution q(θ) and minimizes KL(q||p), which requires computing integrals like E_q[log p(D|θ)] using quadrature or Monte Carlo.

Monte Carlo in ML

ApplicationMethodDescription
Bayesian inferenceMCMC, VISample from posterior distributions
Policy gradientsREINFORCEMonte Carlo estimation of expected returns
Variational autoencodersReparameterizationMonte Carlo gradient estimation
Normalizing flowsImportance samplingMonte Carlo estimation of likelihood
GANsMonte CarloEstimate partition function

Loss Function Integration

Some loss functions require integration: Expected Calibration Error (ECE) integrates over confidence bins, and proper scoring rules integrate over predicted distributions.


Common Mistakes

MistakeProblemSolution
Odd n for Simpson's ruleFormula requires even number of intervalsRound n up to nearest even number
Ignoring error estimateTrusting result without verificationAlways compute error estimate or compare with finer grid
Uniform spacing for peaked functionsWasted evaluations in flat regionsUse adaptive quadrature
Low N for Monte CarloSlow convergence (O(1/√N))Use variance reduction or quasi-Monte Carlo
Integrating over singularitiesInfinite errorTransform variable or split integral
Not checking integrabilityDivergent integral returns wrong valueVerify convergence before trusting result
Using Simpson's for oscillatoryO(h⁴) not enough for highly oscillatoryUse specialized oscillatory quadrature
Ignoring roundoff in long sumsKahan summation may be neededUse compensated summation for many terms

Interview Questions

Q1: Why is Gaussian quadrature more accurate than equally-spaced rules? A: Gaussian quadrature optimizes both node placement and weights to maximize the degree of exactness. With n nodes it integrates polynomials of degree 2n−1 exactly, while equally-spaced rules (Newton-Cotes) with n nodes achieve at most degree n (or n+1 for Simpson's). The trade-off is that Gaussian nodes are irrational, requiring more complex implementation.

Q2: When would you use Monte Carlo over deterministic quadrature? A: Monte Carlo is preferred for high-dimensional integrals (d > 4), where deterministic methods suffer the curse of dimensionality. It is also useful when the integrand is noisy, discontinuous, or only available through simulations. For low-dimensional smooth functions, deterministic methods are far more efficient.

Q3: How does adaptive quadrature decide where to subdivide? A: Adaptive quadrature estimates the local error on each subinterval. If the error exceeds the tolerance, the interval is split (usually in half). The process recurses until all subintervals meet the error tolerance. This concentrates evaluations where the integrand varies rapidly.

Q4: Explain the relationship between numerical integration and numerical differentiation. A: Numerical differentiation amplifies noise (it's an ill-conditioned problem), while numerical integration smooths it (it's well-conditioned). The error in numerical differentiation is O(ε/h) from roundoff plus O(h^p) from truncation, creating an optimal h ≈ √ε. Integration error simply decreases with h.

Q5: What is Richardson extrapolation and how is it used in Romberg integration? A: Richardson extrapolation combines numerical results at different step sizes to eliminate leading error terms. If R(h) = I + c₁h^p + ..., then combining R(h) and R(h/2) eliminates the h^p term, achieving higher accuracy. Romberg integration applies this recursively to trapezoidal rule estimates, achieving exponential convergence for smooth functions.

Q6: How do you handle integrable singularities? A: Strategies include: (1) variable substitution to remove the singularity (e.g., t = √x for x^(-1/2) singularity), (2) splitting the integral and handling the singular part analytically, (3) using Gauss-Jacobi or other specialized quadrature designed for singular weight functions, (4) adaptive quadrature which may concentrate points near the singularity.


Practice Problems

📝Problem 1: Derive Simpson's Rule

Derive Simpson's rule by fitting a parabola through three equally spaced points and integrating it exactly.

💡Solution

import numpy as np
from sympy import symbols, integrate, Poly

# Symbolic derivation
x = symbols('x')

# Fit parabola through (-h, y0), (0, y1), (h, y2)
# Lagrange basis:
# L0 = (x)(x-h) / ((-h)(-2h)) = x(x-h)/(2h²)
# L1 = (x+h)(x-h) / ((h)(-h)) = -(x²-h²)/h²
# L2 = (x)(x+h) / ((2h)(h)) = x(x+h)/(2h²)
# P(x) = y0*L0 + y1*L1 + y2*L2

h_sym = symbols('h', positive=True)
y0, y1, y2 = symbols('y0 y1 y2')

L0 = x * (x - h_sym) / (2 * h_sym**2)
L1 = -(x**2 - h_sym**2) / h_sym**2
L2 = x * (x + h_sym) / (2 * h_sym**2)

P = y0 * L0 + y1 * L1 + y2 * L2

# Integrate from -h to h
result = integrate(P, (x, -h_sym, h_sym))
print(f"∫ P(x) dx from -h to h = {result}")
print(f"Simplified: {result.simplify()}")
# Result: h/3 * (y0 + 4*y1 + y2) — Simpson's 1/3 rule

📝Problem 2: Compare Monte Carlo with Quasi-Monte Carlo

Implement a low-discrepancy sequence (Sobol) and compare convergence with standard Monte Carlo for estimating ∫₀¹∫₀¹ f(x,y) dxdy.

💡Solution

import numpy as np


def monte_carlo_2d(f, n_samples):
    """Standard Monte Carlo integration in 2D."""
    x = np.random.uniform(0, 1, n_samples)
    y = np.random.uniform(0, 1, n_samples)
    return np.mean(f(x, y))


def pseudo_random_2d(f, n_samples):
    """Quasi-Monte Carlo using scrambled Sobol-like sequence."""
    # Simple low-discrepancy: nested uniform grid with random shift
    n_side = int(np.ceil(np.sqrt(n_samples)))
    grid = np.linspace(0, 1, n_side, endpoint=False)
    xx, yy = np.meshgrid(grid, grid)
    x = (xx.ravel()[:n_samples] + np.random.uniform(0, 1/n_side)) % 1
    y = (yy.ravel()[:n_samples] + np.random.uniform(0, 1/n_side)) % 1
    return np.mean(f(x, y))


# Test function
f = lambda x, y: np.sin(np.pi * x) * np.cos(np.pi * y)
exact = (2/np.pi) * 0  # ∫₀¹ sin(πx)dx × ∫₀¹ cos(πy)dy = (2/π) × 0 = 0
# Actually: ∫₀¹ sin(πx) dx = 2/π, ∫₀¹ cos(πy) dy = 0
exact = 0  # So this is a bad test... let me use a different one

f = lambda x, y: x * y + np.sin(2 * np.pi * x)
exact = 0.25  # ∫₀¹∫₀¹ (xy + sin(2πx)) dxdy = 1/4 + 0 = 1/4

print("Monte Carlo vs Quasi-Monte Carlo Convergence")
print(f"{'N':<12} {'MC Error':<14} {'QMC Error':<14}")
print("-" * 40)

for n in [100, 1_000, 10_000, 100_000]:
    np.random.seed(42)
    mc_errors = [abs(monte_carlo_2d(f, n) - exact) for _ in range(5)]
    qmc_errors = [abs(pseudo_random_2d(f, n) - exact) for _ in range(5)]

    print(f"{n:<12} {np.mean(mc_errors):<14.6f} {np.mean(qmc_errors):<14.6f}")

Quick Reference

📋Numerical Integration Quick Reference

MethodNodesError OrderDegree of ExactnessUse Case
Trapezoidaln+1 equally spacedO(h²)1Simple, robust
Simpson'sn+1 (n even)O(h⁴)3Smooth functions
Gauss-LegendrenO(h^(2n))2n−1Maximum accuracy
RombergAdaptiveExponentialHighSmooth functions
Monte CarloN randomO(1/√N)N/AHigh dimensions

Formulas:

  • Trapezoidal: h/2·[f₀ + 2f₁ + ... + 2fₙ₋₁ + fₙ]
  • Simpson's: h/3·[f₀ + 4f₁ + 2f₂ + 4f₃ + ... + fₙ]
  • Gaussian: Σ wᵢf(xᵢ) with Legendre nodes
  • Monte Carlo: V·mean(f(xᵢ))

Error bounds:

  • Trapezoidal: |E| ≤ (b−a)h²/12·max|f''|
  • Simpson's: |E| ≤ (b−a)h⁴/180·max|f⁽⁴⁾|
  • Monte Carlo: SE = σ/√N

Python shortcuts:

  • np.trapz(y, x) — trapezoidal rule
  • scipy.integrate.quad(f, a, b) — adaptive quadrature
  • scipy.integrate.simpson(y, x) — Simpson's rule

Cross-References

  • 086-Floating-Point — Numerical integration error is bounded by floating point precision; long summations need Kahan summation
  • 087-Root-Finding — Adaptive quadrature uses error estimation similar to root-finding convergence criteria
  • 089-Interpolation — Quadrature rules are derived by integrating interpolating polynomials; Gaussian quadrature uses optimal node placement
  • 090-Error Analysis — Truncation error analysis determines the order of convergence for integration methods
  • Monte Carlo Methods — Integration is the foundation of Monte Carlo simulation in physics, finance, and ML
  • ODE Solvers: Every ODE solver (Euler, Runge-Kutta) uses quadrature internally to advance the solution
Lesson Progress88 / 100