Skip to content
The Lumenary

Math laboratory

The harmonic series grows without bound, slowly

Computes Oresme's blocks of the harmonic series exactly (each at least 1/2), finds the first n with 1 + 1/2 + ... + 1/n > t for t = 1..10 using 40-digit decimals (n = 12367 for t = 10), compares with ln n + 0.5772, and contrasts the sum of 1/n^2, which stays below 2. Finite sums show slow growth; divergence is proved in the lesson.

Back to the labExact computationNumerical approximation
The harmonic series grows without bound, slowlyPythonExact computationNumerical approximationComputationally tested
"""The harmonic series: terms that shrink to 0, sums that never stop growing.

Oresme's grouping says the block 1/(2^k + 1) + ... + 1/2^(k+1) is at least 1/2;
we compute these blocks exactly with fractions. Then, with 40-digit decimal
arithmetic, we find the first n with H_n = 1 + 1/2 + ... + 1/n > t for
t = 1, ..., 10 (H_n first exceeds 10 at n = 12367) and compare H_n with
ln n + 0.5772 (Euler's constant). By contrast, 1 + 1/4 + 1/9 + ... stays
below 2.
"""

import math
from decimal import Decimal, localcontext
from fractions import Fraction

#: Euler's constant gamma, to 16 digits (used only for comparison).
EULER_GAMMA = 0.5772156649015329

def oresme_block(k):
    """1/(2^k + 1) + ... + 1/2^(k+1), exactly (2^k terms)."""
    if k < 0:
        raise ValueError("k must be non-negative")
    return sum(Fraction(1, j) for j in range(2 ** k + 1, 2 ** (k + 1) + 1))

def harmonic_exact(n):
    """H_n = 1 + 1/2 + ... + 1/n as a Fraction (for small n)."""
    return sum((Fraction(1, j) for j in range(1, n + 1)), Fraction(0))

def first_exceeding(targets):
    """For each target t (increasing), the least n with H_n > t.

    Uses Decimal with 40 significant digits: after 10^5 additions the
    accumulated rounding error is far below the gaps 1/n near the answers.
    """
    answers = []
    with localcontext() as ctx:
        ctx.prec = 40
        total, n = Decimal(0), 0
        for t in targets:
            while total <= t:
                n += 1
                total += Decimal(1) / n
            answers.append(n)
    return answers

def basel_partial(n):
    """1 + 1/4 + ... + 1/n^2 in floating point (math.fsum)."""
    return math.fsum(1.0 / (j * j) for j in range(1, n + 1))

def main():
    print("Oresme's blocks (exact): each block of 2^k terms is at least 1/2")
    for k in range(5):
        block = oresme_block(k)
        terms = f"1/{2 ** k + 1} + ... + 1/{2 ** (k + 1)}" if k else "1/2"
        print(f"  k = {k}: {terms} = {float(block):.4f}"
              f"  >= 1/2: {block >= Fraction(1, 2)}")
    print("So H(2^m) >= 1 + m/2: the partial sums pass every bound.")
    print()
    targets = list(range(1, 11))
    print("t    first n with H_n > t    ln n + gamma")
    for t, n in zip(targets, first_exceeding(targets)):
        print(f"{t:2d}   {n:12d}             {math.log(n) + EULER_GAMMA:.4f}")
    print("The terms 1/n tend to 0, yet the sums still reach any height -")
    print("it takes roughly e^(t - 0.58) terms to reach height t.")
    print()
    for n in [10, 1000, 100000]:
        print(f"1 + 1/4 + ... + 1/{n}^2 = {basel_partial(n):.6f}   (< 2)")
    print(f"pi^2/6 = {math.pi ** 2 / 6:.6f}, Euler's value of the whole sum")
    print()
    print("Epistemic status: the blocks are exact for k <= 4 (the lesson")
    print("proves the bound for all k); the first-exceeding table uses")
    print("40-digit decimals, numerical but with error far below 1/n; the")
    print("ln n + gamma column is an approximation. No finite table proves")
    print("divergence: the grouping argument does.")

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

Used in