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 2: Numbers Aren't Real - Computer Arithmetic & Cosmic Consequences

Module 1: Foundations of Discrete Computing | ASTR 596

San Diego State University

Learning Outcomes

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


Why This Matters for Astronomy

Astronomical simulations face unique numerical challenges from extreme dynamic ranges within individual problems:

Scale contrasts in typical simulations:

The precision problem: Double precision provides only ~16 decimal digits. This means:

Practical consequences: We must carefully:

Finding Machine Epsilon

Machine epsilon ϵ\epsilon is the smallest number such that 1+ϵ11 + \epsilon \neq 1 in floating-point arithmetic:

import numpy as np

def find_machine_epsilon():
    """
    Find the smallest power of 2 such that 1 + eps != 1
    Note: This finds 2^(-52) for double precision
    """
    eps = 1.0
    while (1.0 + eps/2.0) != 1.0:
        eps = eps / 2.0
    return eps

# Test and compare with NumPy's value
eps_computed = find_machine_epsilon()
eps_numpy = np.finfo(float).eps
print(f"Computed ε: {eps_computed:.3e}")
print(f"NumPy's ε:  {eps_numpy:.3e}")
assert abs(eps_computed - eps_numpy) < 1e-20, "Mismatch in epsilon"

This value determines the fundamental limits of numerical accuracy.

The Three Types of Error

1. Round-off Error (from finite precision)

Round-off error occurs when numbers cannot be represented exactly. The most dangerous situation is catastrophic cancellation when subtracting nearly equal numbers.

Why this matters: In astrophysics, we often compute small differences between large quantities - like parallax angles, proper motions, or energy changes in orbits. Understanding how to reformulate these calculations preserves precision in your results.

import numpy as np

def demonstrate_catastrophic_cancellation():
    """
    Show how subtracting nearly equal numbers destroys precision
    Example: Computing parallax from distance measurements
    """
    # Earth's orbit baseline: small change in distance to star
    d1_au = 1.0000000  # Distance in AU (winter)
    d2_au = 1.0000067  # Distance in AU (summer) - 1000 km difference
    
    def parallax_bad(d1, d2):
        """Naive: loses precision when d1 ≈ d2"""
        if d1 == 0 or d2 == 0:
            raise ValueError("Distance cannot be zero")
        return 1.0/d1 - 1.0/d2
    
    def parallax_good(d1, d2):
        """Reformulated: preserves precision"""
        if d1 == 0 or d2 == 0:
            raise ValueError("Distance cannot be zero")
        return (d2 - d1)/(d1 * d2)
    
    bad_result = parallax_bad(d1_au, d2_au)
    good_result = parallax_good(d1_au, d2_au)
    relative_error = abs(bad_result-good_result)/good_result*100
    
    print(f"Bad method:  {bad_result:.10e}")
    print(f"Good method: {good_result:.10e}")
    print(f"Relative difference: {relative_error:.2f}%")
    
    return bad_result, good_result, relative_error

2. Truncation Error (from approximations)

Truncation error arises from approximating infinite processes with finite ones. For example, truncating the Taylor series for exe^x:

ex1+x+x22!+...+xnn!e^x \approx 1 + x + \frac{x^2}{2!} + ... + \frac{x^n}{n!}

The error is all the neglected terms beyond nn.

3. Propagation Error (accumulation over iterations)

Propagation error is how small errors compound through repeated calculations. This is particularly dangerous in astronomy where we often integrate orbits over millions of timesteps:

import numpy as np

def demonstrate_error_propagation():
    """
    Show how tiny errors grow through repeated operations
    This models what happens in long-term orbital integration
    """
    value = 1.0
    tiny_error = 1e-15  # Below machine precision
    
    # Track error growth
    steps = [1, 10, 100, 1000, 10000, 100000, 1000000]
    results = []
    
    print("Error Propagation in Iterative Calculations:")
    print("Steps    | Total Error | Growth Factor")
    print("-" * 40)
    
    for n in steps:
        result = 1.0
        for _ in range(n):
            result *= (1 + tiny_error)
        
        actual_error = result - 1.0
        growth_factor = actual_error / tiny_error
        results.append((n, actual_error, growth_factor))
        
        print(f"{n:7d} | {actual_error:.3e} | {growth_factor:.1e}")
    
    return results

This is why long-term orbital integrations require special care.

Measuring Errors: Absolute vs. Relative

Why Both Matter

For any computed value xcomputedx_{\text{computed}} and true value xtruex_{\text{true}}, we define:

Eabs=xcomputedxtrue\boxed{E_{\text{abs}} = |x_{\text{computed}} - x_{\text{true}}|}
Erel=xcomputedxtruextrue=Eabsxtrue\boxed{E_{\text{rel}} = \frac{|x_{\text{computed}} - x_{\text{true}}|}{|x_{\text{true}}|} = \frac{E_{\text{abs}}}{|x_{\text{true}}|}}

Key insight: Relative error gives us a scale-independent measure - 1% error means the same thing whether measuring galaxies or atoms. But it breaks down near zero.

Worked Example: Computing Jupiter’s Mass

Let’s calculate both error types step-by-step:

Step 1: Absolute error

Eabs=1.897×1030 g1.898×1030 g=1×1027 gE_{\text{abs}} = |1.897 \times 10^{30} \text{ g} - 1.898 \times 10^{30} \text{ g}| = 1 \times 10^{27} \text{ g}

Step 2: Relative error

Erel=1×1027 g1.898×1030 g=5.3×104=0.053%E_{\text{rel}} = \frac{1 \times 10^{27} \text{ g}}{1.898 \times 10^{30} \text{ g}} = 5.3 \times 10^{-4} = 0.053\%

Conclusion: The 0.05% relative error tells us our accuracy regardless of Jupiter’s enormous mass. For most astrophysical applications, this is excellent precision.

When Each Error Type Matters

Example 1 - Large scales: Computing Earth-Sun distance

Example 2 - Small values: Computing stellar parallax

Example 3 - Near zero: Computing velocity at aphelion

Decision rule: Use relative error when xtrue>1010×xscale|x_{\text{true}}| > 10^{-10} \times x_{\text{scale}}, where xscalex_{\text{scale}} is the typical scale of your problem. Otherwise, use absolute error.

def compute_errors(computed, true, threshold=1e-10):
    """Compute both errors, intelligently choosing which to trust"""
    abs_error = abs(computed - true)
    
    if abs(true) > threshold:
        rel_error = abs_error / abs(true)
        primary = ('relative', rel_error)
    else:
        rel_error = float('inf')  # Undefined but we record it
        primary = ('absolute', abs_error)
    
    return abs_error, rel_error, primary

Tracking Accumulated Error

In iterative algorithms, tiny errors compound. After nn steps with error ϵ\epsilon per step:

Eaccumulatednϵ(if errors are random)E_{\text{accumulated}} \approx n \cdot \epsilon \quad \text{(if errors are random)}
Eaccumulatedϵ(1+ϵ)n1(if errors are systematic)E_{\text{accumulated}} \approx \epsilon \cdot (1 + \epsilon)^n - 1 \quad \text{(if errors are systematic)}
# Systematic error growth (worst case)
value = 1.0
epsilon = 1e-15  # Machine precision error
for n in [10, 1000, 100000, 1000000]:
    accumulated_error = epsilon * ((1 + epsilon)**n - 1)
    print(f"Steps: {n:7d} | Error: {accumulated_error:.3e}")

This is why simulating a galaxy for 10 Gyr requires tolerances of 10-12 or better!

Adaptive Error Control

Algorithms adjust parameters to keep errors below tolerance:

If Eestimated>tolreduce h by factor tol/E\boxed{\text{If } E_{\text{estimated}} > \text{tol} \Rightarrow \text{reduce } h \text{ by factor } \sqrt{\text{tol}/E}}
If Eestimated<tol/10increase h by factor 1.5\boxed{\text{If } E_{\text{estimated}} < \text{tol}/10 \Rightarrow \text{increase } h \text{ by factor 1.5}}
If tol/10Eestimatedtolmaintain h\boxed{\text{If } \text{tol}/10 \leq E_{\text{estimated}} \leq \text{tol} \Rightarrow \text{maintain } h}
def adaptive_h(error_estimate, h_current, tolerance):
    """Adjust step size to maintain target tolerance"""
    if error_estimate > tolerance:
        # Reduce h to bring error under control
        return h_current * 0.5 * np.sqrt(tolerance/error_estimate)
    elif error_estimate < tolerance/10:
        # Safe to increase h for efficiency
        return h_current * 1.5
    else:
        # Error is acceptable
        return h_current

Error Tolerances in Practice

ApplicationTypical TolerancePhysical Justification
Planetary orbits (< 1 yr)10-10Must conserve energy to 10 digits
Planetary orbits (Gyr)10-14Errors accumulate over 1015 timesteps
Stellar evolution10-6Opacity uncertainties dominate
Galaxy mergers10-4Statistical properties matter, not individual stars
MCMC sampling10-8Must satisfy detailed balance exactly
Gravitational waves10-20Strain measurements at detection limit

Convergence Testing Without True Values

In research, you rarely know the true answer. Test convergence by comparing results at different resolutions:

Richardson extrapolation: Eestimated=fhfh/22p1\text{Richardson extrapolation: } E_{\text{estimated}} = \frac{|f_h - f_{h/2}|}{2^p - 1}

where pp is the order of your method (2 for central difference).

# Verify your implementation is correct
h_values = [0.1, 0.05, 0.025, 0.0125]
for i in range(len(h_values)-1):
    ratio = (result[i] - result[i+1]) / (result[i+1] - result[i+2])
    print(f"Convergence ratio: {ratio:.2f} (expect {2**p:.1f})")

Bridge to Part 3: From Machine Arithmetic to Taylor Series

You now understand the three types of numerical error and how they arise from finite precision arithmetic. But how do we systematically analyze and predict these errors in our algorithms?

In Part 3, we’ll explore Taylor series - the mathematical bridge that connects continuous calculus to discrete numerical methods. You’ll discover:

The error analysis techniques you’ve just learned will combine with Taylor series to give you a complete toolkit for understanding and controlling numerical accuracy.

Next: Part 3 - Taylor Series