A Small Taste from My New Book: Season 2 Episode 5

 


In the previous episodes, we developed the analytic machinery needed to connect prime numbers with the complex zeros of the Riemann zeta function. In this episode, we bring those ideas to life. Our focus is the explicit formula for the Chebyshev function ψ(t), a central object in analytic number theory that encodes the distribution of prime powers.

This episode marks a shift from purely theoretical derivations to concrete computation and visualization. We evaluate ψ(t) directly from its arithmetic definition and then reconstruct it using the explicit formula, where primes emerge as a superposition of oscillatory contributions arising from the nontrivial zeros of ζ(s). Under the assumption of the Riemann Hypothesis, these oscillations take a particularly transparent and wave-like form.

By comparing the exact Chebyshev function with truncated versions of the explicit formula, we gain intuition for how much information is carried by finitely many zeta zeros and how their collective interference shapes the observed irregularities in prime distribution. This perspective reveals the primes not as random objects, but as the result of a precise spectral decomposition.

The goal of this episode is twofold: to make the explicit formula computable, and to make its meaning visible. In doing so, we bridge analysis, computation, and intuition—setting the stage for deeper investigations into the zeros themselves in the episodes that follow.

Previously, we established the approximate explicit formula

where

is the Chebyshev -(psi)-function, defined as the cumulative sum of the von Mangoldt function    (see Season2, Episode4).

Assuming that the nontrivial zeros of the Riemann zeta function lie on the critical line, each zero can be written as

Pairing complex conjugate zeros yields a real-valued expression,

This is the explicit formula for the Chebyshev function. It expresses a prime-counting object in terms of a smooth main term  and a correction built from the zeros of the zeta function.

The shocking part: Primes are being reconstructed from the zeros of a complex function.

It is a wave decomposition. Each term is a wave in . So the formula says:  linear growth  interference pattern of infinitely many waves.

This is like a sound wave being decomposed into frequencies. A signal being reconstructed from Fourier modes. Here, frequencies are the imaginary parts of zeta zeros and the signal is the distribution of primes.

To gain insight into this formula, we compute  directly from its definition on the left-hand side and compare it with a truncated version of the right-hand side, obtained by summing over finitely many zeta zeros. The resulting approximations can then be visualized through suitable plots.

#pragma once

#include <algorithm>
#include <cmath>
#include <vector>

namespace mangoldt
{
    using int64 = long long;
    using mangoldt_lambda = std::vector<double>; // Λ(n)
    using chebyshev_psi = std::vector<double>;   // ψ(n) = sum_{k=1}^n Λ(k)

    class mangoldt_cfg
    {
    public:
        static constexpr int MAX_VALUE = 50000000;
        explicit mangoldt_cfg(int64 x) : x_(x)
        {
            // Choose sieve bound L ~ x^(2/3)
            long double raw_bound = 100.0L;
            if (x > 1)
                raw_bound = std::pow(static_cast<long double>(x), 2.0L / 3.0L);

            raw_bound = std::clamp(raw_bound, 100.0L, static_cast<long double>(MAX_VALUE));
            L_ = static_cast<int>(raw_bound);
        }
        int64 input() const noexcept { return x_; }
        int sieve_bound() const noexcept { return L_; }

    private:
        int64 x_;
        int L_;

        friend void compute_lambda_psi(const mangoldt_cfg &, mangoldt_lambda *, chebyshev_psi *);
    };

    class chebyshev_function
    {
    public:
        explicit chebyshev_function(int64 x) { init(x); }

        // Get Λ(n) for small n
        double lambda_value(int64 n)
        {
            if (n <= 1)
                return 0.0;
            if (n <= L)
                return lambda[static_cast<std::size_t>(n)];
            return compute_lambda_direct(n);
        }

        // Get ψ(n) = sum_{k=1}^n Λ(k)
        double psi_value(int64 n)
        {
            if (n <= 0)
                return 0.0;
            if (n <= L)
                return psi[static_cast<std::size_t>(n)];

            if (n < tail_n_)
            {
                double res = psi[static_cast<std::size_t>(L)];
                for (int64 i = L + 1; i <= n; ++i)
                    res += compute_lambda_direct(i);
                return res;
            }

            for (int64 i = tail_n_ + 1; i <= n; ++i)
                tail_psi_ += compute_lambda_direct(i);

            tail_n_ = n;
            return tail_psi_;
        }

    private:
        mangoldt_lambda lambda;
        chebyshev_psi psi;
        int L;
        int64 tail_n_;
        double tail_psi_;

        void init(int64 x)
        {
            mangoldt_cfg cfg(x);
            L = cfg.sieve_bound();

            compute_lambda_psi(cfg, &lambda, &psi);
            tail_n_ = L;
            tail_psi_ = psi[static_cast<std::size_t>(L)];
        }

        double compute_lambda_direct(int64 n)
        {
            if (n <= 1)
                return 0.0;

            for (int64 p = 2; p * p <= n; ++p)
            {
                if (n % p == 0)
                {
                    int64 temp = n;
                    while (temp % p == 0)
                        temp /= p;
                    if (temp == 1)
                        return std::log(static_cast<double>(p));
                    return 0.0;
                }
            }
            return std::log(static_cast<double>(n)); // n is prime
        }
    };

    inline void compute_lambda_psi(const mangoldt_cfg &cfg, mangoldt_lambda *lambda, chebyshev_psi *psi)
    {
        int L = cfg.sieve_bound();
        lambda->assign(L + 1, 0.0);

        std::vector<int> primes;
        std::vector<int> minPrime(L + 1, 0);

        for (int i = 2; i <= L; ++i)
        {
            if (minPrime[i] == 0)
            {
                minPrime[i] = i;
                primes.push_back(i);
                (*lambda)[i] = std::log(static_cast<double>(i));
            }
            for (int p : primes)
            {
                int64 v = 1LL * p * i;
                if (v > L)
                    break;
                minPrime[v] = p;
                if (i % p == 0)
                {
                    int temp = i;
                    int power = 1;
                    while (temp % p == 0)
                    {
                        temp /= p;
                        power++;
                    }
                    if (temp == 1)
                    {
                        (*lambda)[v] = std::log(static_cast<double>(p));
                    }
                    else
                    {
                        (*lambda)[v] = 0.0;
                    }
                    break;
                }
                else
                {
                    (*lambda)[v] = 0.0;
                }
            }
        }

        // Build ψ(n) = sum_{k=1}^n Λ(k)
        psi->assign(L + 1, 0.0);
        for (int i = 1; i <= L; ++i)
            (*psi)[i] = (*psi)[i - 1] + (*lambda)[i];
    }
}

 Von Mangoldt Function & Chebyshev's ψ — Code Walkthrough

This C++ code implements two classical objects from analytic number theory:


The Von Mangoldt Function Λ(n)

Λ(n) is defined as:

  • log(p) if n = p for some prime p and integer k ≥ 1
  • 0 otherwise

So it "fires" only at prime powers, and its value is always the log of the underlying prime.


The Chebyshev ψ Function

ψ(n) is simply the cumulative sum of Λ:

ψ(n) = Λ(1) + Λ(2) + ... + Λ(n)

It's a key tool in the proof of the Prime Number Theorem — ψ(x) ~ x is essentially equivalent to PNT.


How the Code Works

The implementation has three main pieces:

  1. mangoldt_cfg — A configuration object that takes an input x and computes a sieve bound L ~ x^(2/3). Values up to L are precomputed; values beyond that are handled on-the-fly.
  2. compute_lambda_psi — The core sieve. It uses a linear sieve (each composite is visited exactly once via its smallest prime factor) to fill a lambda[] array up to L, then does a prefix sum to build psi[]. This runs in O(L) time.
  3. chebyshev_function — The public-facing class. It preloads the sieve on construction, then answers queries:
    • For n ≤ L: direct array lookup — O(1).
    • For n > L: falls back to compute_lambda_direct(n), which trial-divides n to check if it's a prime power — O(√n).
    • It also caches the running tail sum in tail_psi_ to avoid recomputing from scratch on sequential large queries.

Key Design Choice

The x^(2/3) sieve bound is a classic analytic number theory trade-off: sieving further costs more memory and setup time, but reduces how often the slower O(√n) fallback is needed. For most use cases, this hits a sweet spot between precomputation cost and query speed.


How to use

Create a main.cpp file and add:

void save_chebyshev(chebyshev_function &cf, int64 x, const string &filename = "chebyshev.dat")
{
    ofstream fout(filename);
    if (!fout)
    {
        cerr << "Error: cannot open file " << filename << " for writing\n";
        return;
    }

    for (int64 n = 1; n <= x; ++n)
    {
        fout << n << " " << cf.psi_value(n) << "\n";
    }

    fout.close();
    cout << "Chebyshev function values saved to " << filename << "\n";
}

Then in main

int main()
{

    x=55000;
    chebyshev_function cf(x);
    save_chebyshev(cf, x);

    return 0;
}


Therefore, the values of the Chebyshev function are stored in chebychev.dat. You can then use your preferred plotting software to generate the corresponding graphs.


Using the data obtained from the code above we obtain:



We can also visualize its evolution through animation.



Using the truncated explicit formula

and retaining the first 60 nontrivial zeros of the Riemann zeta function, we obtain the following approximation:



Epilogue (Season2, Episode5)

What we have done in this episode is to bring the explicit formula out of abstraction and into direct contact with computation. The Chebyshev function ψ(t), defined purely in terms of prime powers, has been evaluated numerically and then reconstructed using only the zeros of the Riemann zeta function. The agreement between these two viewpoints is not a coincidence—it is the analytic heart of the Prime Number Theorem and the deepest reason the Riemann Hypothesis matters.

Seen through this lens, the explicit formula is not merely a theoretical statement. It is a spectral decomposition of the primes. Each nontrivial zero contributes an oscillatory mode, and the observed irregularities in the distribution of primes arise from the interference of infinitely many such waves. When we truncate the sum over zeros, we do not destroy the structure—we simply smooth it, much like filtering high frequencies from a signal.

The numerical experiments performed here illustrate a crucial point: primes are not random, nor are they simple. Their structure is encoded globally in the analytic behavior of ζ(s). The zeros are not auxiliary objects—they are the carriers of arithmetic information. Computing ψ(t) directly and comparing it with its reconstruction from zeta zeros makes this relationship tangible.

In the next episode, we will move beyond visualization and return to analysis. The tools developed here will be used to study the density and distribution of zeros themselves, closing the loop between primes, complex analysis, and asymptotic control. What began as an abstract contour integral has now become an explicit bridge between computation and theory—and that bridge is the central theme of analytic number theory.

 

Readers who wish to build a solid foundation in analytic number theory and Fourier analysis, and to understand these ideas from first principles, are encouraged to consult my books The Riemann Hypothesis Revealed and The Essential Transform Toolkit. Both are written with the explicit aim of carefully reconstructing the analytic framework, filling in omitted steps, and making the underlying techniques fully transparent.

 

PS. The first 100 roots of zeta here: https://www.plouffe.fr/simon/constants/zeta100.html


Comments