Skip to content
The Lumenary

Math laboratory

Riemann sums for the parabola, exactly

Computes left, right and midpoint Riemann sums of x^2 on [0, 1] in exact rational arithmetic, confirms the closed form (n + 1)(2n + 1)/(6n^2) for every n up to 200 and the exact errors 1/(2n) + 1/(6n^2) and -1/(12n^2), compares with the antiderivative x^3/3 (SymPy), and shows signed area for sin on [0, 2 pi] numerically.

Back to the labExact computationExhaustive finite checkSymbolic computationNumerical approximation
Riemann sums for the parabola, exactlyPythonExact computationExhaustive finite checkSymbolic computationNumerical approximationComputationally tested
"""Riemann sums for the area under y = x^2, computed exactly.

With fractions there is no rounding at all: the right Riemann sum of x^2 on
[0, 1] with n strips is exactly (n + 1)(2n + 1)/(6n^2), its error is exactly
1/(2n) + 1/(6n^2), and the midpoint sum misses 1/3 by exactly 1/(12n^2). The
fundamental theorem (via SymPy) gives the limit 1/3 at once. A last check
shows that integrals are signed: midpoint sums of sin on [0, 2 pi] tend to 0.
"""

import math
from fractions import Fraction

import sympy

def riemann_sum(f, a, b, n, rule="right"):
    """Sum of n strips of width (b - a)/n; heights at the left end, right
    end or midpoint of each strip. Exact when f, a, b are rational."""
    if n < 1:
        raise ValueError("n must be at least 1")
    offset = {"left": Fraction(0), "right": Fraction(1), "midpoint": Fraction(1, 2)}
    if rule not in offset:
        raise ValueError("rule must be left, right or midpoint")
    width = (b - a) / n
    return sum(f(a + (k + offset[rule]) * width) for k in range(n)) * width

def square(x):
    """f(x) = x^2."""
    return x * x

def right_closed_form(n):
    """(n + 1)(2n + 1) / (6 n^2), the right sum of x^2 on [0, 1]."""
    return Fraction((n + 1) * (2 * n + 1), 6 * n * n)

def antiderivative_check():
    """SymPy: an antiderivative of x^2 and the integral over [0, 1]."""
    x = sympy.symbols("x")
    return sympy.integrate(x ** 2, x), sympy.integrate(x ** 2, (x, 0, 1))

def main():
    zero, one = Fraction(0), Fraction(1)
    third = Fraction(1, 3)
    agree = all(
        riemann_sum(square, zero, one, n) == right_closed_form(n)
        for n in range(1, 201)
    )
    print(f"Right sum = (n+1)(2n+1)/(6n^2) exactly for n = 1..200: {agree}")
    print()
    print("   n   left sum      right sum     midpoint      right - 1/3")
    for n in [1, 2, 4, 10, 100]:
        left = riemann_sum(square, zero, one, n, "left")
        right = riemann_sum(square, zero, one, n, "right")
        mid = riemann_sum(square, zero, one, n, "midpoint")
        print(f"{n:4d}   {float(left):.8f}    {float(right):.8f}    "
              f"{float(mid):.8f}    {str(right - third)}")
    n = 100
    err_right = riemann_sum(square, zero, one, n) - third
    err_mid = riemann_sum(square, zero, one, n, "midpoint") - third
    print(f"n = 100: right error = 1/(2n) + 1/(6n^2)? "
          f"{err_right == Fraction(1, 2 * n) + Fraction(1, 6 * n * n)}")
    print(f"n = 100: midpoint error = -1/(12n^2)? "
          f"{err_mid == Fraction(-1, 12 * n * n)}")
    print()
    anti, area = antiderivative_check()
    print(f"SymPy: antiderivative {anti}, so the area is F(1) - F(0) = {area}")
    print()
    signed = riemann_sum(math.sin, 0.0, 2 * math.pi, 1000, "midpoint")
    above = riemann_sum(math.sin, 0.0, math.pi, 1000, "midpoint")
    print(f"sin on [0, 2pi], 1000 strips: |midpoint sum| < 1e-12? "
          f"{abs(signed) < 1e-12}")
    print(f"Signed area cancels: the part above the axis alone is {above:.6f}")
    print("(the exact value of that part is 2).")
    print()
    print("Epistemic status: the fraction sums are exact and the closed form")
    print("is checked for n = 1..200 only (the lesson proves it for all n);")
    print("SymPy's integral is symbolic; the sine sums are floating point.")

if __name__ == "__main__":
    main()
Runs entirely in your browser, in an isolated worker. Nothing is sent to a server.

Used in