Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Part 1: The Fundamental Paradox - Calculus on Computers

Foundations of Discrete Computing | Numerical Methods Module 1 | ASTR 596

San Diego State University

Learning Outcomes

By the end of this section, you will be able to:


Introduction: Why Numerical Derivatives Matter

Before diving into technical details, let’s understand why numerical derivatives and finite precision arithmetic are foundational to computational astrophysics. Nearly every calculation you’ll perform depends on these concepts.

Derivatives are everywhere in astrophysics:

Why numerical methods are essential: Most astrophysical problems cannot be solved analytically. Consider the three-body problem - there’s no closed-form solution for the general case. Instead, we must use numerical approximations, which means:

The computational challenge: In practice, astrophysical simulations must handle enormous dynamic ranges that push the limits of finite precision arithmetic:

Double precision provides only ~16 decimal digits. We cannot simultaneously represent these extreme scales with full precision. We must understand:

This section provides the rigorous foundation you need to write reliable code for astrophysical simulations. The principles you learn here will apply to every project in this course and throughout your research career.


The Core Problem

Recall that the mathematical definition of a derivative of a function f(x)f(x) at a point xx is:

f(x)=limh0f(x+h)f(x)hf'(x) = \lim_{h \to 0} \frac{f(x+h) - f(x)}{h}

where hh represents a small displacement from the point xx. This definition relies on taking the limit as hh approaches zero - we evaluate the slope of secant lines through points that get arbitrarily close together until we obtain the slope of the tangent line at xx. The derivative is fundamental to physics, describing velocities, accelerations, and all forms of change.

The derivative paradox: mathematical definition requires h \to 0, but computers cannot take true limits. Left: analytical derivative (smooth tangent line). Right: numerical approximation using finite h values showing convergence - smaller h=0.1 approaches the true slope better than larger h=0.3.

Figure 1:The derivative paradox: mathematical definition requires h0h \to 0, but computers cannot take true limits. Left: analytical derivative (smooth tangent line). Right: numerical approximation using finite hh values showing convergence - smaller h=0.1h=0.1 approaches the true slope better than larger h=0.3h=0.3.

On a computer, this mathematical ideal encounters a fundamental obstacle. Computers represent numbers using floating-point arithmetic with finite precision (typically 64 bits for a double-precision number).

This floating-point arithmetic means:

  1. We cannot represent h=0h = 0 exactly - Division by zero would cause the program to crash or return undefined/infinite values.

  2. We cannot make hh arbitrarily small - Below a certain threshold (around 10-16 for double precision), the computer cannot distinguish between xx and x+hx+h due to rounding. When we try to compute f(x+h)f(x)f(x+h) - f(x), both values round to the same floating-point number, giving us 0/h=00/h = 0, which is completely wrong.

  3. Even moderate values of hh cause problems - When hh is very small (say 10-10), and f(x+h)f(x)f(x+h) \approx f(x), we’re subtracting two nearly equal numbers. Due to finite precision, we might only retain a few significant digits in the difference, leading to catastrophic cancellation.

This catastrophic cancellation is one of the most common sources of numerical error in scientific computing.

Therefore, we must approximate the derivative using a finite difference:

f(x)f(x+h)f(x)hfor some finite h>0f'(x) \approx \frac{f(x+h) - f(x)}{h} \quad \text{for some finite } h > 0

This finite difference approximation creates a fundamental trade-off:

The optimal value of hh must balance these competing effects. Through rigorous analysis (which we’ll derive shortly), this optimal value turns out to be approximately hϵmachine108h \approx \sqrt{\epsilon_{\text{machine}}} \approx 10^{-8} for forward differences, where ϵmachine2.2×1016\epsilon_{\text{machine}} \approx 2.2 \times 10^{-16} is the machine epsilon for double precision.

This machine epsilon represents the fundamental limit of floating-point precision. This fundamental limitation means that on a computer, we can typically only compute derivatives to about 8 digits of accuracy using forward differences, even though our numbers have 16 digits of precision. This is why understanding numerical methods is crucial.

The Complete Finite Difference Landscape

Now that we understand the fundamental challenge, let’s explore all the ways we can approximate derivatives numerically. Each method emerges from different manipulations of Taylor series, and each has distinct advantages and trade-offs.

Intuition: Why Taylor Series?

Taylor series answers the question: “How does a function change near a point?” It builds the function from its derivatives, like constructing a curve from its slope, curvature, and higher-order bending.

The Taylor series expansion of a function f(x)f(x) around a point x0x_0 is:

f(x)=f(x0)+f(x0)(xx0)+f(x0)2!(xx0)2+f(x0)3!(xx0)3+f(x) = f(x_0) + f'(x_0)(x-x_0) + \frac{f''(x_0)}{2!}(x-x_0)^2 + \frac{f'''(x_0)}{3!}(x-x_0)^3 + \cdots

Or more compactly:

f(x)=n=0f(n)(x0)n!(xx0)n\boxed{f(x) = \sum_{n=0}^{\infty} \frac{f^{(n)}(x_0)}{n!}(x-x_0)^n}

Each term adds more detail: the first gives the value, the second adds the slope, the third adds curvature, and so on. For numerical methods, we truncate this infinite series after a few terms, which introduces truncation error. By understanding which terms we keep and which we discard, we can predict exactly what error our approximations introduce. Think of it as a mathematical microscope that reveals the fine structure of functions.

Forward Difference: Looking Ahead

The forward difference method uses information about the function at the current point xx and a point ahead at x+hx+h. To understand its accuracy, we start with the Taylor expansion of f(x+h)f(x+h) around the point xx:

f(x+h)=f(x)+hf(x)+h22f(x)+h36f(x)+O(h4)f(x+h) = f(x) + hf'(x) + \frac{h^2}{2}f''(x) + \frac{h^3}{6}f'''(x) + O(h^4)

This expansion tells us exactly how f(x+h)f(x+h) relates to the function value and derivatives at xx. Now, if we solve for f(x)f'(x):

f(x)=f(x+h)f(x)hh2f(x)h26f(x)+O(h3)f'(x) = \frac{f(x+h) - f(x)}{h} - \frac{h}{2}f''(x) - \frac{h^2}{6}f'''(x) + O(h^3)

This reveals that the forward difference approximation:

f(x)f(x+h)f(x)h\boxed{f'(x) \approx \frac{f(x+h) - f(x)}{h}}

has a leading error term of h2f(x)-\frac{h}{2}f''(x). This is a first-order method because the error is proportional to h1h^1. The negative sign tells us that for functions with positive second derivatives (convex functions), forward difference underestimates the true derivative.

Backward Difference: Looking Behind

The backward difference method uses the current point xx and a point behind at xhx-h. Following the same Taylor series approach:

f(xh)=f(x)hf(x)+h22f(x)h36f(x)+O(h4)f(x-h) = f(x) - hf'(x) + \frac{h^2}{2}f''(x) - \frac{h^3}{6}f'''(x) + O(h^4)

Rearranging to isolate f(x)f'(x):

f(x)=f(x)f(xh)h+h2f(x)h26f(x)+O(h3)f'(x) = \frac{f(x) - f(x-h)}{h} + \frac{h}{2}f''(x) - \frac{h^2}{6}f'''(x) + O(h^3)

The backward difference approximation:

f(x)f(x)f(xh)h\boxed{f'(x) \approx \frac{f(x) - f(x-h)}{h}}

has a leading error term of +h2f(x)+\frac{h}{2}f''(x), opposite in sign to forward difference! This means backward difference overestimates the derivative for convex functions.

Central Difference: The Sweet Spot

The central difference method is where mathematical elegance meets practical advantage. By using points symmetrically placed around xx, something remarkable happens. Let’s expand both f(x+h)f(x+h) and f(xh)f(x-h):

f(x+h)=f(x)+hf(x)+h22f(x)+h36f(x)+h424f(4)(x)+O(h5)f(x+h) = f(x) + hf'(x) + \frac{h^2}{2}f''(x) + \frac{h^3}{6}f'''(x) + \frac{h^4}{24}f^{(4)}(x) + O(h^5)
f(xh)=f(x)hf(x)+h22f(x)h36f(x)+h424f(4)(x)+O(h5)f(x-h) = f(x) - hf'(x) + \frac{h^2}{2}f''(x) - \frac{h^3}{6}f'''(x) + \frac{h^4}{24}f^{(4)}(x) + O(h^5)

When we subtract these two expressions:

f(x+h)f(xh)=2hf(x)+2h36f(x)+O(h5)f(x+h) - f(x-h) = 2hf'(x) + \frac{2h^3}{6}f'''(x) + O(h^5)

Notice that all even-order derivative terms cancel exactly! This is due to the symmetry of the method. Solving for f(x)f'(x):

f(x)=f(x+h)f(xh)2hh26f(x)+O(h4)f'(x) = \frac{f(x+h) - f(x-h)}{2h} - \frac{h^2}{6}f'''(x) + O(h^4)

The central difference approximation:

f(x)f(x+h)f(xh)2h\boxed{f'(x) \approx \frac{f(x+h) - f(x-h)}{2h}}

is second-order accurate with a leading error term of h26f(x)-\frac{h^2}{6}f'''(x). This quadratic dependence on hh means that halving the step size reduces the error by a factor of 4, compared to only a factor of 2 for forward or backward differences.

The Power of Symmetry

Symmetry in numerical methods often leads to cancellation of systematic errors. This principle extends beyond finite differences to integration methods (Simpson’s rule), PDE solvers (centered schemes), and even Monte Carlo methods (antithetic variates). When you have a choice, symmetric methods typically provide better accuracy for the same computational cost. The central difference method exploits this symmetry to achieve higher accuracy without additional function evaluations.

Higher-Order Methods: When You Need More Accuracy

For applications requiring higher precision, we can use more points to achieve fourth-order accuracy. Through careful manipulation of Taylor series, we can derive:

f(x)=f(x+2h)+8f(x+h)8f(xh)+f(x2h)12h+O(h4)\boxed{f'(x) = \frac{-f(x+2h) + 8f(x+h) - 8f(x-h) + f(x-2h)}{12h} + O(h^4)}

The coefficients (1,8,8,1)/12(-1, 8, -8, 1)/12 are specifically chosen to eliminate the O(h)O(h), O(h2)O(h^2), and O(h3)O(h^3) error terms.

Beyond Fourth Order: Diminishing Returns

The pattern continues: sixth-order methods use 7 points, eighth-order use 9 points. However, for noisy data or non-smooth functions, higher-order methods can amplify errors rather than reduce them. Additionally, near boundaries or discontinuities, you may not have enough points for high-order methods. This is why second-order central difference remains the default choice in practice - it provides an excellent balance of accuracy, robustness, and simplicity.

Error scaling comparison for finite difference methods applied to f(x)=\sin(x) at x=1. Forward (dashed) and backward (solid) differences both show O(h) convergence, central difference (dotted) shows O(h^2), and 4th-order central (solid) shows O(h^4) convergence in the theoretical regime. All methods eventually hit the machine epsilon floor (\approx 10^{-16}) where round-off dominates. Gray reference lines show exact theoretical scaling.

Figure 2:Error scaling comparison for finite difference methods applied to f(x)=sin(x)f(x)=\sin(x) at x=1x=1. Forward (dashed) and backward (solid) differences both show O(h)O(h) convergence, central difference (dotted) shows O(h2)O(h^2), and 4th-order central (solid) shows O(h4)O(h^4) convergence in the theoretical regime. All methods eventually hit the machine epsilon floor (1016\approx 10^{-16}) where round-off dominates. Gray reference lines show exact theoretical scaling.

Implementation: Compact Code Examples

Here’s a minimal implementation of all finite difference methods:

import numpy as np

def finite_diff(f, x, h, method='central'):
    """All finite difference methods in one place"""
    try:
        if method == 'forward':
            return (f(x + h) - f(x)) / h
        elif method == 'backward':
            return (f(x) - f(x - h)) / h
        elif method == 'central':
            return (f(x + h) - f(x - h)) / (2 * h)
        elif method == 'central4':
            return (-f(x+2*h) + 8*f(x+h) - 8*f(x-h) + f(x-2*h)) / (12*h)
        else:
            raise ValueError(f"Unknown method: {method}")
    except (ZeroDivisionError, OverflowError) as e:
        raise ValueError(f"Numerical error in {method} difference: {e}")

The Fundamental Trade-off: Rigorous Derivation of Optimal hh

We’ve established that choosing hh involves balancing truncation error (from approximating the derivative) and round-off error (from finite precision arithmetic). Let’s now rigorously derive the optimal value of hh for each method.

Forward Difference: Optimal Step Size

For the forward difference method, we have two sources of error:

  1. Truncation error: Etrunc=h2f(x)E_{\text{trunc}} = \frac{h}{2}|f''(x)|

  2. Round-off error: Eroundϵf(x)hE_{\text{round}} \approx \frac{\epsilon |f(x)|}{h}

The total error is:

Etotal(h)=h2f(x)+ϵf(x)hE_{\text{total}}(h) = \frac{h}{2}|f''(x)| + \frac{\epsilon |f(x)|}{h}

To find the optimal hh, we minimize this by taking the derivative with respect to hh:

dEtotaldh=f(x)2ϵf(x)h2=0\frac{dE_{\text{total}}}{dh} = \frac{|f''(x)|}{2} - \frac{\epsilon |f(x)|}{h^2} = 0

Solving for hh:

hopt=2ϵf(x)f(x)h_{\text{opt}} = \sqrt{\frac{2\epsilon |f(x)|}{|f''(x)|}}

For functions where f(x)|f(x)| and f(x)|f''(x)| are of similar magnitude:

hoptϵ1.5×108h_{\text{opt}} \approx \sqrt{\epsilon} \approx 1.5 \times 10^{-8}

This is a remarkable result: even though our numbers have 16 digits of precision, we can only compute derivatives to about 8 digits of accuracy using forward differences!

Central Difference: Even Better Optimal Step Size

For central difference, the error components are:

  1. Truncation error: Etrunc=h26f(x)E_{\text{trunc}} = \frac{h^2}{6}|f'''(x)|

  2. Round-off error: Eround=ϵf(x)hE_{\text{round}} = \frac{\epsilon |f(x)|}{h}

Following the same minimization procedure:

hopt=(3ϵf(x)f(x))1/3ϵ1/36×106h_{\text{opt}} = \left(\frac{3\epsilon |f(x)|}{|f'''(x)|}\right)^{1/3} \approx \epsilon^{1/3} \approx 6 \times 10^{-6}

This is crucial: central difference allows a step size that is about 300 times larger than forward difference while achieving better accuracy!

Visualizing the Error Trade-off

Why this matters: The competition between truncation and round-off error creates a fundamental limit on numerical accuracy. This visualization shows why we can’t just make hh arbitrarily small - there’s an optimal value where total error is minimized. Understanding this trade-off is crucial for choosing appropriate step sizes in your simulations.

The fundamental trade-off in numerical derivatives showing why h cannot be arbitrarily small. Thick lines show component errors: truncation error decreases as O(h) for forward differences and O(h^2) for central differences, while round-off error increases as O(\epsilon/h). Total errors (dotted lines) have minima at optimal h values: \sqrt{2\epsilon} for forward and (6\epsilon)^{1/3} for central differences.

Figure 3:The fundamental trade-off in numerical derivatives showing why hh cannot be arbitrarily small. Thick lines show component errors: truncation error decreases as O(h)O(h) for forward differences and O(h2)O(h^2) for central differences, while round-off error increases as O(ϵ/h)O(\epsilon/h). Total errors (dotted lines) have minima at optimal hh values: 2ϵ\sqrt{2\epsilon} for forward and (6ϵ)1/3(6\epsilon)^{1/3} for central differences.

For large hh, truncation error dominates. For small hh, round-off error dominates. The optimal hh sits at the bottom of the U.

Practical Algorithm for Choosing hh

Why this matters: In real applications, you won’t know the derivatives f(x)f''(x) or f(x)f'''(x) needed for the theoretical optimal hh. This practical algorithm estimates appropriate step sizes based on the problem scale and machine precision. This is what you’ll actually use in your research code.

import numpy as np

def practical_optimal_h(x, method='central'):
    """
    Choose h based on method and problem scale
    
    The key insight: h should scale with |x| to maintain
    relative precision for large values
    """
    epsilon = 2.2e-16
    
    if abs(x) < epsilon:
        raise ValueError(f"x too close to zero for reliable derivatives")
    
    x_scale = max(abs(x), 1.0)  # Avoid division issues near zero
    
    if method in ['forward', 'backward']:
        h_opt = np.sqrt(epsilon) * x_scale
    elif method == 'central':
        h_opt = epsilon**(1/3) * x_scale
    elif method == 'central4':
        h_opt = epsilon**(1/5) * x_scale
    else:
        raise ValueError(f"Unknown method: {method}")
    
    # Ensure h is not too small relative to x
    h_final = max(h_opt, epsilon * abs(x))
    return h_final

# Example: Choosing h for different problem scales
x_values = [1.0, 1.496e13, 1.989e33]  # dimensionless, 1 AU, 1 M_sun
for x in x_values:
    h = practical_optimal_h(x, 'central')
    print(f"x = {x:.2e}, optimal h = {h:.2e}, h/x = {h/x:.2e}")

Bridge to Part 2: From Derivatives to Machine Arithmetic

You now understand the fundamental paradox: computers cannot take true limits, forcing us to balance truncation and round-off error. But where does this round-off error come from?

In Part 2, we’ll dive deep into how computers represent numbers. You’ll discover:

The derivative methods you’ve just learned are built on the foundation of floating-point arithmetic. Understanding this foundation will help you write more robust code and diagnose mysterious numerical failures in your simulations.

Next: Part 2 - Numbers Aren’t Real