Three larger runs, putting the whole pipeline together at \(e^{20}\) and \(e^{21}\): many interval lengths at once, the alternative prediction \(F^*\) on stage, and a nested animation. The heavy computations were run once and saved; the cells that took minutes to hours are displayed but not executed, and everything downstream retrieves from the database. The folded cell below defines the pick helper of Section 4.2.

Setup: numeric display and the pick helper
import numpy as np

# NumPy 2 displays scalars inside containers as np.float64(...); restore the
# plain numeric display so statistics read cleanly.
np.set_printoptions(legacy="1.25")


def pick(found, lower_bound):
    """Return the retrieved dataset whose data begins at the given lower bound.

    Introduced in the Datasets chapter: accepts either return shape of
    ``retrieve`` (one dataset, or a list of them).
    """
    if isinstance(found, dict):
        found = [found]
    return next(d for d in found if d['header']['lower_bound'] == lower_bound)
import matplotlib.pyplot as plt

import primes_in_intervals as pii
from primes_in_intervals import (
    analyze,
    compare,
    intervals,
    nest,
    plot_distribution_frame,
    retrieve,
    save,
)

9.1 Example 4

# In this example, we'll try our alternative estimate F*(H,m,lambda*), lambda* = H/log N.
N = int(np.exp(20))
HH = [20,30,40,50,60,70,80,90,100,200]
step = 10**3
A = N - 10**5
B = N + 10**5
C = list(range(A,B+1,step))

Ten interval lengths over one pass each of half a billion integers: the computation below took a little under an hour, so it is shown, not run.

exp20 = { H : {} for H in HH}
start = timer()
for H in HH:
    exp20[H] = intervals(C,H,interval_type='overlap')
end = timer()
end - start 
3146.539329699939
for H in HH:
    save(exp20[H])

Retrieval brings all ten datasets back. Other datasets share some of these interval lengths, so, as always, we select by lower bound with pick rather than by position; the printed summaries list everything each length matched.

getexp20 = {}
for H in HH:
    getexp20[H] = pick(retrieve(H), A)
Found 2 datasets corresponding to interval of length 20 (overlap intervals).

 [0] 'header' : {'interval_type': 'overlap', 'lower_bound': 485065195, 'upper_bound': 485265195, 'interval_length': 20, 'no_of_checkpoints': 201, 'contents': ['data']}


 [1] 'header' : {'interval_type': 'overlap', 'lower_bound': 1318715734, 'upper_bound': 1318915734, 'interval_length': 20, 'no_of_checkpoints': 200, 'contents': ['data']}

Found 2 datasets corresponding to interval of length 30 (overlap intervals).

 [0] 'header' : {'interval_type': 'overlap', 'lower_bound': 485065195, 'upper_bound': 485265195, 'interval_length': 30, 'no_of_checkpoints': 201, 'contents': ['data']}


 [1] 'header' : {'interval_type': 'overlap', 'lower_bound': 1318715734, 'upper_bound': 1318915734, 'interval_length': 30, 'no_of_checkpoints': 200, 'contents': ['data']}

Found 2 datasets corresponding to interval of length 40 (overlap intervals).

 [0] 'header' : {'interval_type': 'overlap', 'lower_bound': 485065195, 'upper_bound': 485265195, 'interval_length': 40, 'no_of_checkpoints': 201, 'contents': ['data']}


 [1] 'header' : {'interval_type': 'overlap', 'lower_bound': 1318715734, 'upper_bound': 1318915734, 'interval_length': 40, 'no_of_checkpoints': 200, 'contents': ['data']}

Found 2 datasets corresponding to interval of length 50 (overlap intervals).

 [0] 'header' : {'interval_type': 'overlap', 'lower_bound': 485065195, 'upper_bound': 485265195, 'interval_length': 50, 'no_of_checkpoints': 201, 'contents': ['data']}


 [1] 'header' : {'interval_type': 'overlap', 'lower_bound': 1318715734, 'upper_bound': 1318915734, 'interval_length': 50, 'no_of_checkpoints': 200, 'contents': ['data']}

Found 3 datasets corresponding to interval of length 60 (overlap intervals).

 [0] 'header' : {'interval_type': 'overlap', 'lower_bound': 2669017, 'upper_bound': 3869017, 'interval_length': 60, 'no_of_checkpoints': 1201, 'contents': ['data']}


 [1] 'header' : {'interval_type': 'overlap', 'lower_bound': 485065195, 'upper_bound': 485265195, 'interval_length': 60, 'no_of_checkpoints': 201, 'contents': ['data']}


 [2] 'header' : {'interval_type': 'overlap', 'lower_bound': 1318715734, 'upper_bound': 1318915734, 'interval_length': 60, 'no_of_checkpoints': 200, 'contents': ['data']}

Found 2 datasets corresponding to interval of length 70 (overlap intervals).

 [0] 'header' : {'interval_type': 'overlap', 'lower_bound': 485065195, 'upper_bound': 485265195, 'interval_length': 70, 'no_of_checkpoints': 201, 'contents': ['data']}


 [1] 'header' : {'interval_type': 'overlap', 'lower_bound': 1318715734, 'upper_bound': 1318915734, 'interval_length': 70, 'no_of_checkpoints': 200, 'contents': ['data']}

Found 2 datasets corresponding to interval of length 80 (overlap intervals).

 [0] 'header' : {'interval_type': 'overlap', 'lower_bound': 485065195, 'upper_bound': 485265195, 'interval_length': 80, 'no_of_checkpoints': 201, 'contents': ['data']}


 [1] 'header' : {'interval_type': 'overlap', 'lower_bound': 1318715734, 'upper_bound': 1318915734, 'interval_length': 80, 'no_of_checkpoints': 200, 'contents': ['data']}

Found 3 datasets corresponding to interval of length 90 (overlap intervals).

 [0] 'header' : {'interval_type': 'overlap', 'lower_bound': 65559979, 'upper_bound': 65759959, 'interval_length': 90, 'no_of_checkpoints': 203, 'contents': ['data']}


 [1] 'header' : {'interval_type': 'overlap', 'lower_bound': 485065195, 'upper_bound': 485265195, 'interval_length': 90, 'no_of_checkpoints': 201, 'contents': ['data']}


 [2] 'header' : {'interval_type': 'overlap', 'lower_bound': 1318715734, 'upper_bound': 1318915734, 'interval_length': 90, 'no_of_checkpoints': 200, 'contents': ['data']}

Found 3 datasets corresponding to interval of length 100 (overlap intervals).

 [0] 'header' : {'interval_type': 'overlap', 'lower_bound': 2000000, 'upper_bound': 3000000, 'interval_length': 100, 'no_of_checkpoints': 101, 'contents': ['data']}


 [1] 'header' : {'interval_type': 'overlap', 'lower_bound': 485065195, 'upper_bound': 485265195, 'interval_length': 100, 'no_of_checkpoints': 201, 'contents': ['data']}


 [2] 'header' : {'interval_type': 'overlap', 'lower_bound': 1318715734, 'upper_bound': 1318915734, 'interval_length': 100, 'no_of_checkpoints': 200, 'contents': ['data']}

Found 1 dataset corresponding to interval of length 200 (overlap intervals).

 'header' : {'interval_type': 'overlap', 'lower_bound': 485065195, 'upper_bound': 485265195, 'interval_length': 200, 'no_of_checkpoints': 201, 'contents': ['data']}
for H in HH:
    analyze(getexp20[H])
for H in HH:
    compare(getexp20[H])

Now the plot, for \(H = 90\): this is the figure where the alternative pair takes the stage, \(\mathrm{Binom}(H, \lambda^*/H)\) and \(F^*(H,m,\lambda^*)\) with \(\lambda^* = H/\log N\), in place of the usual pair. The final frame’s text box states the range in its centered form, so we pass the overlay as a literal string.

H = 90 # 20,30,40,50,70,80,200
X = getexp20[H]

A = X['header']['lower_bound']
H = X['header']['interval_length']
C = list(X['distribution'].keys())
B = C[-1]

N = (A + B)/2
p_alt = 1/np.log(N)
mu = X['statistics'][B]['mean']
sigma = X['statistics'][B]['var']
med = X['statistics'][B]['med']
if med == int(med):
    med = int(med)
modes = X['statistics'][B]['mode']

overlay_text = (fr'$X = \pi(a + H) - \pi(a)$' + '\n\n'
                + fr'$N - M < a \leq N + M$' + '\n\n'
                + fr'$H = {H}$' + '\n\n'
                + fr'$N = [\exp(20)]$' + '\n\n'
                + fr'$M = {int(B - N)}$' + '\n\n'
                + fr'$\lambda^* = H/\log N = {H*p_alt:.5f}$' + '\n\n'
                + r'$\mathbb{E}[X] = $' + f'{mu:.5f}' + '\n\n'
                + r'$\mathrm{Var}(X) = $' + f'{sigma:.5f}' + '\n\n'
                + fr'median : ${med}$' + '\n\n'
                + fr'mode(s): ${modes}$')

plt.rcParams.update({'font.size': 22})
fig, ax = plt.subplots(figsize=(22, 11))
fig.suptitle('Primes in intervals')
plot_distribution_frame(ax, X, B,
                        show_binom=False, show_binom_alt=True,
                        show_frei=False, show_frei_alt=True,
                        overlay=overlay_text, overlay_position=(0.74, 0.18))
plt.rcParams['savefig.facecolor'] = 'white'
plt.show()

9.2 Example 5

The same window about \(e^{20}\), now at the single length \(H = 76\). The computation ran once (several minutes) and was saved:

N = int(np.exp(20))
M = 10**5
H = 76
A = N - M
B = N + M
step = 10**3
C = list(range(A, B + 1, step))
N_exp20_H_76 = intervals(C, H)
N_exp20_H_76 = pick(retrieve(76), 485065195)
Found 1 dataset corresponding to interval of length 76 (overlap intervals).

 'header' : {'interval_type': 'overlap', 'lower_bound': 485065195, 'upper_bound': 485265195, 'interval_length': 76, 'no_of_checkpoints': 201, 'contents': ['data']}
analyze(N_exp20_H_76)['header']
{'interval_type': 'overlap',
 'lower_bound': 485065195,
 'upper_bound': 485265195,
 'interval_length': 76,
 'no_of_checkpoints': 201,
 'contents': ['data', 'distribution', 'statistics']}

The plot is the standard pair, \(\mathrm{Binom}(H, \lambda/H)\) against \(F(H,m,\lambda)\), with the centered overlay on the final frame.

X = N_exp20_H_76

A = X['header']['lower_bound']
H = X['header']['interval_length']
C = list(X['distribution'].keys())
B = C[-1]

N = (A + B)/2
p = 1/(np.log(N) - 1)
mu = X['statistics'][B]['mean']
sigma = X['statistics'][B]['var']
med = X['statistics'][B]['med']
if med == int(med):
    med = int(med)
modes = X['statistics'][B]['mode']

overlay_text = (fr'$X = \pi(a + H) - \pi(a)$' + '\n\n'
                + fr'$N - M < a \leq N + M$' + '\n\n'
                + fr'$H = {H}$' + '\n\n'
                + fr'$N = [\exp(20)]$' + '\n\n'
                + fr'$M = {int(B - N)}$' + '\n\n'
                + fr'$\lambda = H/(\log N - 1) = {H*p:.5f}$' + '\n\n'
                + r'$\mathbb{E}[X] = $' + f'{mu:.5f}' + '\n\n'
                + r'$\mathrm{Var}(X) = $' + f'{sigma:.5f}' + '\n\n'
                + fr'median : ${med}$' + '\n\n'
                + fr'mode(s): ${modes}$')

fig, ax = plt.subplots(figsize=(22, 11))
fig.suptitle('Primes in intervals')
plot_distribution_frame(ax, X, B, overlay=overlay_text, overlay_position=(0.74, 0.18))
plt.show()

The full animation is saved with save_gif or save_mp4 (or pii animate); the frames step through the two hundred checkpoints of the window.

9.3 Example 6 (nested intervals)

The largest run: nested intervals about \(e^{21}\), thirteen interval lengths from \(20\) to \(140\). The checkpoints are placed symmetrically about \(N\), a thousand apart, with \(N\) itself excluded, so that nesting pairs them off exactly.

N = int(np.exp(21))
K = 199
k = (K - 1)//2 # = 99
Delta = 10**3 
D = []
for j in range(k + 1):
    D.extend([N - (j + 1)*Delta, N + (j + 1)*Delta])
C = sorted(D)
HH = list(range(20,141,10))
EXP21 = {H : {} for H in HH}
for H in HH:
    EXP21[H] = intervals(C, H)
# Takes a few hours...
for H in HH:
    save(EXP21[H])
EXP21 = {}
for H in HH:
    EXP21[H] = pick(retrieve(H), C[0])
Found 2 datasets corresponding to interval of length 20 (overlap intervals).

 [0] 'header' : {'interval_type': 'overlap', 'lower_bound': 485065195, 'upper_bound': 485265195, 'interval_length': 20, 'no_of_checkpoints': 201, 'contents': ['data']}


 [1] 'header' : {'interval_type': 'overlap', 'lower_bound': 1318715734, 'upper_bound': 1318915734, 'interval_length': 20, 'no_of_checkpoints': 200, 'contents': ['data']}

Found 2 datasets corresponding to interval of length 30 (overlap intervals).

 [0] 'header' : {'interval_type': 'overlap', 'lower_bound': 485065195, 'upper_bound': 485265195, 'interval_length': 30, 'no_of_checkpoints': 201, 'contents': ['data']}


 [1] 'header' : {'interval_type': 'overlap', 'lower_bound': 1318715734, 'upper_bound': 1318915734, 'interval_length': 30, 'no_of_checkpoints': 200, 'contents': ['data']}

Found 2 datasets corresponding to interval of length 40 (overlap intervals).

 [0] 'header' : {'interval_type': 'overlap', 'lower_bound': 485065195, 'upper_bound': 485265195, 'interval_length': 40, 'no_of_checkpoints': 201, 'contents': ['data']}


 [1] 'header' : {'interval_type': 'overlap', 'lower_bound': 1318715734, 'upper_bound': 1318915734, 'interval_length': 40, 'no_of_checkpoints': 200, 'contents': ['data']}

Found 2 datasets corresponding to interval of length 50 (overlap intervals).

 [0] 'header' : {'interval_type': 'overlap', 'lower_bound': 485065195, 'upper_bound': 485265195, 'interval_length': 50, 'no_of_checkpoints': 201, 'contents': ['data']}


 [1] 'header' : {'interval_type': 'overlap', 'lower_bound': 1318715734, 'upper_bound': 1318915734, 'interval_length': 50, 'no_of_checkpoints': 200, 'contents': ['data']}

Found 3 datasets corresponding to interval of length 60 (overlap intervals).

 [0] 'header' : {'interval_type': 'overlap', 'lower_bound': 2669017, 'upper_bound': 3869017, 'interval_length': 60, 'no_of_checkpoints': 1201, 'contents': ['data']}


 [1] 'header' : {'interval_type': 'overlap', 'lower_bound': 485065195, 'upper_bound': 485265195, 'interval_length': 60, 'no_of_checkpoints': 201, 'contents': ['data']}


 [2] 'header' : {'interval_type': 'overlap', 'lower_bound': 1318715734, 'upper_bound': 1318915734, 'interval_length': 60, 'no_of_checkpoints': 200, 'contents': ['data']}

Found 2 datasets corresponding to interval of length 70 (overlap intervals).

 [0] 'header' : {'interval_type': 'overlap', 'lower_bound': 485065195, 'upper_bound': 485265195, 'interval_length': 70, 'no_of_checkpoints': 201, 'contents': ['data']}


 [1] 'header' : {'interval_type': 'overlap', 'lower_bound': 1318715734, 'upper_bound': 1318915734, 'interval_length': 70, 'no_of_checkpoints': 200, 'contents': ['data']}

Found 2 datasets corresponding to interval of length 80 (overlap intervals).

 [0] 'header' : {'interval_type': 'overlap', 'lower_bound': 485065195, 'upper_bound': 485265195, 'interval_length': 80, 'no_of_checkpoints': 201, 'contents': ['data']}


 [1] 'header' : {'interval_type': 'overlap', 'lower_bound': 1318715734, 'upper_bound': 1318915734, 'interval_length': 80, 'no_of_checkpoints': 200, 'contents': ['data']}

Found 3 datasets corresponding to interval of length 90 (overlap intervals).

 [0] 'header' : {'interval_type': 'overlap', 'lower_bound': 65559979, 'upper_bound': 65759959, 'interval_length': 90, 'no_of_checkpoints': 203, 'contents': ['data']}


 [1] 'header' : {'interval_type': 'overlap', 'lower_bound': 485065195, 'upper_bound': 485265195, 'interval_length': 90, 'no_of_checkpoints': 201, 'contents': ['data']}


 [2] 'header' : {'interval_type': 'overlap', 'lower_bound': 1318715734, 'upper_bound': 1318915734, 'interval_length': 90, 'no_of_checkpoints': 200, 'contents': ['data']}

Found 3 datasets corresponding to interval of length 100 (overlap intervals).

 [0] 'header' : {'interval_type': 'overlap', 'lower_bound': 2000000, 'upper_bound': 3000000, 'interval_length': 100, 'no_of_checkpoints': 101, 'contents': ['data']}


 [1] 'header' : {'interval_type': 'overlap', 'lower_bound': 485065195, 'upper_bound': 485265195, 'interval_length': 100, 'no_of_checkpoints': 201, 'contents': ['data']}


 [2] 'header' : {'interval_type': 'overlap', 'lower_bound': 1318715734, 'upper_bound': 1318915734, 'interval_length': 100, 'no_of_checkpoints': 200, 'contents': ['data']}

Found 1 dataset corresponding to interval of length 110 (overlap intervals).

 'header' : {'interval_type': 'overlap', 'lower_bound': 1318715734, 'upper_bound': 1318915734, 'interval_length': 110, 'no_of_checkpoints': 200, 'contents': ['data']}

Found 1 dataset corresponding to interval of length 120 (overlap intervals).

 'header' : {'interval_type': 'overlap', 'lower_bound': 1318715734, 'upper_bound': 1318915734, 'interval_length': 120, 'no_of_checkpoints': 200, 'contents': ['data']}

Found 1 dataset corresponding to interval of length 130 (overlap intervals).

 'header' : {'interval_type': 'overlap', 'lower_bound': 1318715734, 'upper_bound': 1318915734, 'interval_length': 130, 'no_of_checkpoints': 200, 'contents': ['data']}

Found 1 dataset corresponding to interval of length 140 (overlap intervals).

 'header' : {'interval_type': 'overlap', 'lower_bound': 1318715734, 'upper_bound': 1318915734, 'interval_length': 140, 'no_of_checkpoints': 200, 'contents': ['data']}
NEXP21 = {}
for H in HH:
    NEXP21[H] = nest(EXP21[H])
for H in HH:
    analyze(NEXP21[H])

The plot, for \(H = 60\), is the nested version: each frame is one of the hundred nested intervals, the innermost first, the sample growing outward from the midpoint, with the vertical axis rounded at the third decimal for these smaller proportions.

# HH = [20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140]
X = NEXP21[HH[4]]

H = X['header']['interval_length']
C = list(X['distribution'].keys())
c = C[-1]

A = c[0]
B = c[1]
N = (A + B)//2
p = 1/(np.log(N) - 1)
mu = X['statistics'][c]['mean']
sigma = X['statistics'][c]['var']
med = X['statistics'][c]['med']
if med == int(med):
    med = int(med)
modes = X['statistics'][c]['mode']

overlay_text = (fr'$X = \pi(a + H) - \pi(a)$' + '\n\n'
                + fr'$N - M < a \leq N + M$' + '\n\n'
                + fr'$H = {H}$' + '\n\n'
                + r'$N = [e^{21}]$' + '\n\n'
                + fr'$M = 10^5$' + '\n\n'
                + fr'$\lambda = H/(\log N - 1) = {H*p:.5f}$' + '\n\n'
                + r'$\mathbb{E}[X] = $' + f'{mu:.5f}' + '\n\n'
                + r'$\mathrm{Var}(X) = $' + f'{sigma:.5f}' + '\n\n'
                + fr'median : ${med}$' + '\n\n'
                + fr'mode(s): ${modes}$')

plt.rcParams.update({'font.size': 22})
fig, ax = plt.subplots(figsize=(22, 11))
fig.suptitle('Primes in intervals')
plot_distribution_frame(ax, X, c, ylim_decimals=3,
                        overlay=overlay_text, overlay_position=(0.75, 0.15))
plt.rcParams['savefig.facecolor'] = 'white'
plt.show()

The animation, innermost interval outward:

Animation of the distribution of primes in intervals of length 60 about e^21, the nested sample growing outward from the midpoint, with the Binomial and F prediction curves overlaid.

Animated distribution of primes in overlapping intervals of length 60 in nested ranges about e^21.

9.4 Supplementary examples

Four further examples, at sizes where everything runs in seconds, so every cell below computes live: the Nachlass tables in full, the nested pipeline at \(e^{17}\) for overlapping and for prime-starting intervals, and a closing remark on the choice of \(\lambda\). (These complement Section 7.0.3’s one-line Goldschmidt table.)

Example 1

Tables from Gauss’s Nachlass. Create data for primes in disjoint intervals of length \(100\) (starting at multiples of \(100\)), up to \(10^7\). Checkpoints every \(10^5\).

By the bye, an interval of the form \([100k, 100k + 100)\) is referred to as a “Centade” in Gauss’s Nachlass. Analogous to the term Decade (a 10-year period from years ’0 to ’9), a Centade is a 100-year period from ’00 to ’99.

C = list(range(0,10*10**6 + 1, 10**5))
H = 100
nachlass = pii.intervals(C,H,'disjoint')

Gauss and Goldschmidt summarize their data in tables of primes up to \(1\) million, between \(1\) and \(2\) million, and so on. Let’s emulate that.

NACHLASS = { }
for i in range(1,11):
    NACHLASS[i] = pii.extract(nachlass, [(i - 1)*10**6, i*10**6] , option='narrow')
    pii.partition(NACHLASS[i])

Let’s display one of these tables: the one for primes between \(2\) and \(3\) million.

NACHLASS3df = pii.display(NACHLASS[3], count='partition', orient='columns')
NACHLASS3df
Table 9.1: Interval type: disjoint. Lower bound: 2000000. Upper bound: 3000000. Interval length: 100. Partial counts: non-cumulative.
  2000000 2100000 2200000 2300000 2400000 2500000 2600000 2700000 2800000 2900000 3000000 totals
0 0 0 0 0 0 0 0 1 0 0 0 1
1 0 3 2 2 4 1 3 4 2 2 2 25
2 0 10 9 9 10 9 5 10 7 15 13 97
3 0 32 27 29 33 37 35 28 43 30 43 337
4 0 69 70 73 86 78 88 70 93 84 65 776
5 0 119 145 138 135 146 136 159 137 141 152 1408
6 0 198 183 179 177 193 193 195 195 179 189 1881
7 0 203 201 205 194 190 179 201 188 222 212 1995
8 0 158 167 168 157 151 172 141 145 131 135 1525
9 0 114 110 113 113 102 88 96 86 110 103 1035
10 0 63 52 44 54 56 57 54 68 53 58 559
11 0 21 18 30 29 25 25 22 24 18 15 227
12 0 8 9 10 7 7 13 17 9 7 11 98
13 0 2 4 0 1 5 6 1 2 6 1 28
14 0 0 3 0 0 0 0 1 0 2 0 6
15 0 0 0 0 0 0 0 0 0 0 1 1
17 0 0 0 0 0 0 0 0 1 0 0 1
prime_tally 0 6872 6857 6849 6791 6770 6808 6765 6717 6747 6707 67883

Here’s the original: Gauss/Goldschmidt were only short by \(21\) primes in the end!

Facsimile of the Nachlass table tallying primes per Centade between two and three million.

Gauss’s and Goldschmidt’s tallies for the third million, from the Nachlass.

Example 2

Let’s look at a series of nested intervals centred around \(N = [e^{17}] = 24,154,952\). We take the density of primes close to \(N\) as \(1/(\log N - 1)\), which is \(1/15.999999968802452\ldots\), virtually \(1/16\). We’ll get data for intervals of length \(64, 68, 72, 76, 80\).

N = int(np.exp(17))
HH = [64, 68, 72, 76, 80]
C = list(range(N - 10**4,N + 10**4 + 1, 10**2))
EXP17 = {}
for H in HH:
    EXP17[H] = pii.intervals(C, H, 'overlap')

Right now we have data for the intervals \((N - 10^4, N - 10^4 + 100k]\), \(k = 1,\ldots,100\). Let’s reconfigure the data to consider nested intervals \((N - 100k, N + 100k]\), \(k = 1,\ldots,100\), all centred around \(N\). We’ll analyze the data (get the distributions and statistics) while we’re at it.

EXP17NEST = {}
for H in HH:
    EXP17NEST[H] = pii.nest(EXP17[H])
    pii.analyze(EXP17NEST[H])

Let’s display what we have for \(H = 76\) for instance.

EXP17_76_NESTtable = pii.display(EXP17NEST[76])
EXP17_76_NESTtable.tail(5)
B - A A B H 0 1 2 3 4 5 6 7 8 9 10
95 19200 24145352 24164552 76 140 448 1562 2806 4296 4080 3392 1624 682 144 26
96 19400 24145252 24164652 76 140 448 1570 2836 4368 4134 3428 1624 682 144 26
97 19600 24145152 24164752 76 140 448 1572 2862 4382 4178 3508 1658 682 144 26
98 19800 24145052 24164852 76 140 448 1572 2864 4426 4260 3570 1668 682 144 26
99 20000 24144952 24164952 76 140 448 1584 2892 4550 4288 3578 1668 682 144 26

Let’s compare the data (for \(H = 76\)) to three predictions: the Binomial, our prediction, and the alternate version of our prediction (with the density of primes around \(N\) taken to be \(1/\log N\)). Specifically, with \(\lambda = H/(\log N - 1)\) (and assuming \(\lambda \asymp 1\)), the naive prediction based purely on Cramér’s model is the Binomial distribution \(\mathrm{Binom}(H,\lambda/H)\), whose probability mass function is given by

\[f(m; H, \lambda/H) = \binom{H}{m}\left(\frac{\lambda}{H}\right)^m\left(1 - \frac{\lambda}{H}\right)^{H - m} = \frac{e^{-\lambda}\lambda^m}{m!}\bigg[1 + \frac{Q_1(\lambda,m)}{H} + \frac{Q_2(\lambda,m)}{H^2} + \cdots\bigg],\] where each \(Q_j(\lambda,m)\) is a polynomial in \(\lambda\) and \(m\), and in particular,

\[Q_1(\lambda,m) = \frac{m - (m - \lambda)^2}{2}.\]

Our prediction is

\[F(H,m,\lambda) = \frac{e^{-\lambda}\lambda^m}{m!}\left[1 + \frac{Q_1(\lambda,m)}{H}\left(\log H + (\log 2\pi + \gamma - 1)\right) \right].\]

Our alternative prediction is, with \(\lambda^* = H/\log N\),

\[F^*(H,m,\lambda^*) = \frac{e^{-\lambda^*}(\lambda^*)^m}{m!}\left[1 + \frac{\lambda^*}{H}(m - \lambda^*) + \frac{\log H + (\log 2\pi + \gamma - 1)}{H}Q_1(m,\lambda^*) \right].\]

The table below shows tuples \((a,b,c,d)\), where \(a\) is the actual number, \(b\) is the Binomial-based prediction, \(c\) is our prediction and \(d\) is our alternative prediction. It would be nice to be able to conjecture something about a tertiary term!

We’ll just show the last five rows of the table as it’s a bit long and hard to read.

pii.compare(EXP17NEST[76])
EXP17_76_NESTcompare = pii.display(EXP17NEST[76], comparisons='absolute').tail(5)
EXP17_76_NESTcompare
B - A A B H 0 1 2 3 4 5 6 7 8 9 10
95 19200 24145352 24164552 76 (140, 142, 24, -4) (448, 720, 399, 371) (1562, 1802, 1479, 1535) (2806, 2963, 2960, 3091) (4296, 3605, 3981, 4076) (4080, 3461, 3972, 3953) (3392, 2730, 3094, 2991) (1624, 1820, 1929, 1822) (682, 1046, 964, 903) (144, 527, 370, 358) (26, 235, 89, 105)
96 19400 24145252 24164652 76 (140, 143, 24, -4) (448, 728, 403, 375) (1570, 1820, 1495, 1551) (2836, 2994, 2990, 3123) (4368, 3643, 4022, 4119) (4134, 3497, 4013, 3995) (3428, 2759, 3126, 3022) (1624, 1839, 1949, 1841) (682, 1057, 974, 912) (144, 532, 374, 362) (26, 237, 90, 106)
97 19600 24145152 24164752 76 (140, 145, 24, -4) (448, 735, 407, 379) (1572, 1839, 1510, 1567) (2862, 3025, 3021, 3156) (4382, 3680, 4064, 4161) (4178, 3533, 4054, 4036) (3508, 2787, 3158, 3054) (1658, 1858, 1970, 1860) (682, 1068, 984, 922) (144, 538, 378, 366) (26, 240, 91, 107)
98 19800 24145052 24164852 76 (140, 146, 25, -4) (448, 743, 411, 383) (1572, 1858, 1526, 1583) (2864, 3056, 3052, 3188) (4426, 3718, 4105, 4204) (4260, 3569, 4096, 4077) (3570, 2815, 3191, 3085) (1668, 1877, 1990, 1879) (682, 1079, 994, 931) (144, 543, 382, 370) (26, 242, 92, 108)
99 20000 24144952 24164952 76 (140, 148, 25, -4) (448, 750, 416, 387) (1584, 1877, 1541, 1599) (2892, 3086, 3083, 3220) (4550, 3755, 4147, 4246) (4288, 3605, 4137, 4118) (3578, 2844, 3223, 3116) (1668, 1896, 2010, 1898) (682, 1090, 1004, 940) (144, 549, 385, 373) (26, 245, 93, 110)

We can perhaps work on the formatting of such tables.

Now, we want to know which prediction is the “best”, and this is hard to see by glancing at the above table. By “best” we mean gives the smallest sum-of-squared-error over \(m\) (number of primes in an interval). We’re mainly interested in comparing \(F\) and \(F^*\). Comparing \(F^*\) to the Binomial is not really apples-to-apples because the Binomial we are using takes the probability of finding a prime around \(N\) as being \(\lambda/H = 1/(\log N - 1)\), rather than \(\lambda^*/H = 1/\log N\), as in \(F^*\).

We’ll use our winners function to determine the best predictions for each interval. For each prediction, this function also gives us the \(m\) for which that prediction gives a smaller error than the others.

pii.winners(EXP17NEST[76])
EXP17_76_NESTwinners = pii.display(EXP17NEST[76], winners='show')
EXP17_76_NESTwinners.tail(10)
B - A A B H B sq error F sq error F* sq error 1 2 3 B wins for m in F wins for m in F* wins for m in most wins 2nd most wins least wins
90 18200 24145852 24164052 76 1497275 394200 389041 F* F B [0, 7] [1, 3, 5, 6, 10] [2, 4, 8, 9] F F* B
91 18400 24145752 24164152 76 1516951 421121 417568 F* F B [0, 7] [1, 3, 5, 6, 10] [2, 4, 8, 9] F F* B
92 18600 24145652 24164252 76 1539301 421271 407616 F* F B [0, 7] [1, 3, 5, 6, 10] [2, 4, 8, 9] F F* B
93 18800 24145552 24164352 76 1651297 442710 450597 F F* B [0, 7] [1, 3, 5, 6, 10] [2, 4, 8, 9] F F* B
94 19000 24145452 24164452 76 1760940 477133 478442 F F* B [0, 7] [1, 3, 5, 6, 10] [2, 4, 8, 9] F F* B
95 19200 24145352 24164552 76 1816405 473749 474031 F F* B [0, 7] [1, 3, 5, 6, 10] [2, 4, 8, 9] F F* B
96 19400 24145252 24164652 76 1926743 518268 508866 F* F B [0, 7] [1, 3, 5, 6, 10] [2, 4, 8, 9] F F* B
97 19600 24145152 24164752 76 1998950 530791 541328 F F* B [0, 7] [1, 3, 5, 6, 10] [2, 4, 8, 9] F F* B
98 19800 24145052 24164852 76 2161638 587660 612378 F F* B [0, 7] [1, 3, 5, 6, 10] [2, 4, 8, 9] F F* B
99 20000 24144952 24164952 76 2282457 647032 645987 F* F B [0, 7] [1, 3, 5, 6, 10] [2, 4, 8, 9] F F* B

Finally, let’s make an animated plot, with one frame for each of the intervals considered. Here is its final frame, drawn live:

# HH = [64, 68, 72, 76, 80]
X = EXP17NEST[HH[3]]

H = X['header']['interval_length']
C = list(X['distribution'].keys())
c = C[-1]

A = c[0]
B = c[1]
N = (A + B)//2
p = 1/(np.log(N) - 1)
mu = X['statistics'][c]['mean']
sigma = X['statistics'][c]['var']
med = X['statistics'][c]['med']
if med == int(med):
    med = int(med)
modes = X['statistics'][c]['mode']

overlay_text = (fr'$X = \pi(a + H) - \pi(a)$' + '\n\n'
                + fr'$N - M < a \leq N + M$' + '\n\n'
                + fr'$H = {H}$' + '\n\n'
                + r'$N = [e^{17}]$' + '\n\n'
                + fr'$M = 10^4$' + '\n\n'
                + fr'$\lambda = H/(\log N - 1) = {H*p:.5f}$' + '\n\n'
                + r'$\mathbb{E}[X] = $' + f'{mu:.5f}' + '\n\n'
                + r'$\mathrm{Var}(X) = $' + f'{sigma:.5f}' + '\n\n'
                + fr'median : ${med}$' + '\n\n'
                + fr'mode(s): ${modes}$')

plt.rcParams.update({'font.size': 22})
fig, ax = plt.subplots(figsize=(22, 11))
fig.suptitle('Primes in intervals')
plot_distribution_frame(ax, X, c, ylim_decimals=3,
                        overlay=overlay_text, overlay_position=(0.75, 0.15))
plt.rcParams['savefig.facecolor'] = 'white'
plt.show()

And the animation itself:

Animation of the distribution of primes in intervals of length 76 about e^17, the nested sample growing outward, with the Binomial and F curves overlaid.

Animated distribution of primes in overlapping intervals of length 76 in nested ranges about e^17.

Example 3

Let’s go through the steps of Example 2, but instead of counting all intervals of the form \((a, a + H]\) as \(a\) runs over all integers in the range \((N - M, N + M]\), let’s consider only intervals of the form \((p, p + H]\) as \(p\) runs over only the primes in the range \((N - M, N + M]\). We have not yet worked through the details of the second-order term in our prediction in this case, but up to first order approximation we should still have \(e^{-\lambda}\lambda^m/m!\)

First we generate the data using the 'prime_start' option in our intervals function.

N = int(np.exp(17))
HH = [64, 68, 72, 76, 80]
C = list(range(N - 10**4,N + 10**4 + 1, 10**2))
PSEXP17 = {}
for H in HH:
    PSEXP17[H] = pii.intervals(C, H, 'prime_start')

Let’s centre the intervals around \(N\) with the nest function.

PSEXP17NEST = {}
for H in HH:
    PSEXP17NEST[H] = pii.nest(PSEXP17[H])
    pii.analyze(PSEXP17NEST[H])

Let’s have a look at the data in a table.

PSEXP17_76_NESTtable = pii.display(PSEXP17NEST[76])
PSEXP17_76_NESTtable.tail(5)
pi(B) - pi(A) A B H 0 1 2 3 4 5 6 7 8 9
95 1160 24145352 24164552 76 8 35 102 187 269 268 178 80 29 4
96 1172 24145252 24164652 76 8 35 103 190 273 272 178 80 29 4
97 1187 24145152 24164752 76 8 35 104 191 275 277 183 81 29 4
98 1201 24145052 24164852 76 8 35 104 192 282 281 185 81 29 4
99 1212 24144952 24164952 76 8 35 105 196 287 282 185 81 29 4

Although our predictions have only been worked out for the case of overlapping intervals, and the secondary term in the case of disjoint/left-endpoint-prime intervals might well be a bit different, we can nevertheless compare the data to these “predictions”…

pii.compare(PSEXP17NEST[76])
PSEXP17_76_NESTcompare = pii.display(PSEXP17NEST[76], comparisons='absolute').tail(5)
PSEXP17_76_NESTcompare
pi(B) - pi(A) A B H 0 1 2 3 4 5 6 7 8 9
95 1160 24145352 24164552 76 (8, 8, 1, 0) (35, 43, 24, 22) (102, 108, 89, 92) (187, 179, 178, 186) (269, 217, 240, 246) (268, 209, 239, 238) (178, 164, 186, 180) (80, 109, 116, 110) (29, 63, 58, 54) (4, 31, 22, 21)
96 1172 24145252 24164652 76 (8, 8, 1, 0) (35, 44, 24, 22) (103, 110, 90, 93) (190, 180, 180, 188) (273, 220, 243, 248) (272, 211, 242, 241) (178, 166, 188, 182) (80, 111, 117, 111) (29, 63, 58, 55) (4, 32, 22, 21)
97 1187 24145152 24164752 76 (8, 8, 1, 0) (35, 44, 24, 22) (104, 111, 91, 94) (191, 183, 183, 191) (275, 222, 246, 252) (277, 213, 245, 244) (183, 168, 191, 184) (81, 112, 119, 112) (29, 64, 59, 55) (4, 32, 22, 22)
98 1201 24145052 24164852 76 (8, 8, 1, 0) (35, 45, 24, 23) (104, 112, 92, 96) (192, 185, 185, 193) (282, 225, 249, 255) (281, 216, 248, 247) (185, 170, 193, 187) (81, 113, 120, 113) (29, 65, 60, 56) (4, 32, 23, 22)
99 1212 24144952 24164952 76 (8, 8, 1, 0) (35, 45, 25, 23) (105, 113, 93, 96) (196, 187, 186, 195) (287, 227, 251, 257) (282, 218, 250, 249) (185, 172, 195, 188) (81, 114, 121, 115) (29, 66, 60, 57) (4, 33, 23, 22)

…and see which not-really-a-prediction fares best…

pii.winners(PSEXP17NEST[76])
PSEXP17_76_NESTwinners = pii.display(PSEXP17NEST[76], winners='show')
PSEXP17_76_NESTwinners.tail(10)
pi(B) - pi(A) A B H B sq error F sq error F* sq error 1 2 3 B wins for m in F wins for m in F* wins for m in most wins 2nd most wins least wins
90 1102 24145852 24164052 76 7830 3912 2947 F* F B [0, 1, 2, 7] [5] [3, 4, 6, 7, 8, 9] F* B F
91 1113 24145752 24164152 76 7811 3917 2997 F* F B [0, 1, 2, 7] [5] [3, 4, 6, 7, 8, 9] F* B F
92 1122 24145652 24164252 76 8102 4103 3099 F* F B [0, 1, 2, 7] [5] [3, 4, 6, 7, 8, 9] F* B F
93 1136 24145552 24164352 76 8706 4353 3409 F* F B [0, 1, 2, 7] [5, 9] [3, 4, 6, 7, 8, 9] F* B F
94 1147 24145452 24164452 76 9201 4711 3569 F* F B [0, 1, 2, 7] [5] [3, 4, 6, 7, 8, 9] F* B F
95 1160 24145352 24164552 76 9271 4627 3581 F* F B [0, 1, 2, 7] [5] [3, 4, 6, 8, 9] F* B F
96 1172 24145252 24164652 76 9805 4873 3865 F* F B [0, 1, 2, 7] [5] [3, 4, 6, 7, 8, 9] F* B F
97 1187 24145152 24164752 76 10294 5000 3913 F* F B [0, 1, 2, 7] [5, 9] [3, 4, 6, 7, 8, 9] F* B F
98 1201 24145052 24164852 76 11016 5448 4239 F* F B [0, 1, 2, 7] [5] [2, 3, 4, 6, 7, 8, 9] F* B F
99 1212 24144952 24164952 76 11409 5735 4552 F* F B [0, 1, 2, 7] [1, 5] [3, 4, 6, 8, 9] F* B F

And now we can grok everything with an animation… Actually, these not-necessarily-predictions look half-decent, given that there are only about \(1000\) intervals being used for the data. Here, all four curves are drawn, the standard pair and the alternative pair, with a caveat boxed at the top; the final frame:

# HH = [64, 68, 72, 76, 80]
X = PSEXP17NEST[HH[3]]

H = X['header']['interval_length']
C = list(X['distribution'].keys())
c = C[-1]

A = c[0]
B = c[1]
N = (A + B)//2
PI = sum(X['nested_interval_data'][c].values())
p = 1/(np.log(N) - 1)
p_alt = 1/(np.log(N))
mu = X['statistics'][c]['mean']
sigma = X['statistics'][c]['var']
med = X['statistics'][c]['med']
if med == int(med):
    med = int(med)
modes = X['statistics'][c]['mode']

overlay_text = (fr'$X = \pi(p + H) - \pi(p)$' + '\n\n'
                + fr'$N - M < p \leq N + M$, $p$ prime' + '\n\n'
                + fr'$\pi(N + M) - \pi(N - M) = {PI}$' + '\n\n'
                + fr'$H = {H}$, '
                + r'$N = [e^{17}]$, '
                + fr'$M = 10^4$' + '\n\n'
                + fr'$\lambda = H/(\log N - 1) = {H*p:.5f}$' + '\n\n'
                + fr'$\lambda^* = H/\log N = {H*p_alt:.5f}$' + '\n\n'
                + r'$\mathbb{E}[X] = $' + f'{mu:.5f}' + '\n\n'
                + r'$\mathrm{Var}(X) = $' + f'{sigma:.5f}' + '\n\n'
                + fr'median : ${med}$' + '\n\n'
                + fr'mode(s): ${modes}$')

fig, ax = plt.subplots(figsize=(22, 11))
fig.suptitle('Primes in intervals')
plot_distribution_frame(ax, X, c,
                        show_binom_alt=True, show_frei=True, show_frei_alt=True,
                        note='NB: $F$ and $F^*$ might not be applicable' + '\n' + 'in this case, without modification.',
                        overlay=overlay_text, overlay_position=(0.75, 0.15))
plt.show()

Animation of the distribution of primes in intervals of length 76 whose left endpoints are prime, about e^17, with all four prediction curves overlaid and a caveat that F and F* may not apply.

Animated distribution of primes in prime-starting intervals of length 76 in nested ranges about e^17.

Example 4

We use \(\lambda = H/(\log N - 1)\) in our prediction \(F(H,m,\lambda)\) because the density of primes close to \(N\) is approximately \(1/(\log N - 1)\), by the prime number theorem. Actually, the prime number theorem says that the number of primes up to \(N\) is well-approximated by

\[\int_2^N \frac{dt}{\log t} = \frac{N}{\log N} + \frac{N}{(\log N)^2} + \frac{2N}{(\log N)^3} + \cdots .\]

Thus, the average density of primes up to \(N\) is approximated by

\[\frac{1}{N} \int_2^N \frac{dt}{\log t} = \frac{1}{\log N} + \frac{1}{(\log N)^2} + \frac{2}{(\log N)^3} + \cdots .\]

(Indeed, Gauss’s observation from his numerical data was that the density of primes around \(n\) is around \(1/\log n\), so that the number of primes around \(N\) may be estimated by integrating over this density.) Notice that

\[\frac{1}{\log N - 1} = \frac{1}{\log N} + \frac{1}{(\log N)^2} + \frac{1}{(\log N)^3} + \cdots, \]

and so \(1/(\log N - 1)\) is a good proxy for what the prime number theorem gives us: more precisely,

\[\frac{1}{N} \int_2^N \frac{dt}{\log t} = \frac{1}{\log N - 1} + O\left(\frac{1}{(\log N)^3}\right).\]

In fact, the details leading to our prediction \(F(H,m,\lambda)\) are unaffected if, instead of letting \(\lambda = H/(\log N - 1)\), we let \(\lambda = H/(\log N - 1 + \epsilon(N))\), where \(\epsilon(N) \to 0\) rapidly enough. (This is also related to the reason why, if we let \(\lambda^* = H/\log N\), which only approximates what we get from the logarithmic integral up to an error of \(O(1/(\log N)^2)\), we get a different form for our prediction, which we are calling \(F^*(H,m,\lambda^*)\).) That is, we can use better approximations than \(1/(\log N - 1)\) to the density of primes close to \(N\), but this does not affect the form of our prediction \(F(H,m,\lambda)\), because \(\epsilon(N)\) gets absorbed into our error terms. If we could work out or conjecture tertiary and higher order terms in our prediction \(F(H,m,\lambda)\), this would become quite relevant and interesting.

Notwithstanding, we can still play around with \(\lambda\) in our numerics. The prime number theorem suggests that if we are considering primes in intervals around \((N - M, N + M]\), we should take the density of primes (or “probability” of a random integer in the interval being prime) as

\[\frac{1}{2M} \int_{N - M}^{N + M} \frac{dt}{\log t}.\]

Again, letting this be \(\lambda/H\) doesn’t affect the form of our prediction \(F(H,m,\lambda)\). Below is an animated plot, where we chose \(N\) so that the above with \(M = 10^6\) is very close to \(1/20\).

Animation of the distribution of primes in intervals of length 100, the density parameter chosen via the logarithmic integral to be very close to one twentieth.

Animated distribution for intervals of length 100 about an N chosen so that the logarithmic-integral density over the window is very close to 1/20.

What we should really like to do is, instead of fixing \(H\) and letting \(\lambda = H/(\log N - 1)\) (or any of the alternatives above), and considering intervals close enough to \(N\) that \(\lambda\) remains more or less constant, is fix \(\lambda\), and let \(H = \lambda \log n\) (say), where we are considering intervals of the form \((n, n + H]\). Then we’d have \(\lambda/H = 1/\log n\), and this is what the prime number theorem (and Gauss) really say: the density of primes around an integer \(n\) is \(1/\log n\). Alas, for now, the argument leading to our prediction relies on the fact that \(H\) is a fixed integer of size of order \(\log N\).