codexprime-sieve-steps.py
prime-sieve-steps.py
"""Draw all four sieve passes through 60; write the PNG in the working directory.
Run from _media: MPLBACKEND=Agg python ../prime-sieve-steps.py
Tested with Python 3.14.4 and Matplotlib 3.10.7.
"""
from math import isqrt
from pathlib import Path
import matplotlib.pyplot as plt
from matplotlib.patches import Patch, Rectangle
def sieve_steps(limit):
removed = set()
steps = []
for p in range(2, isqrt(limit) + 1):
if p in removed:
continue
marked = set(range(p * p, limit + 1, p))
newly = marked - removed
steps.append((p, removed.copy(), newly))
removed.update(marked)
return steps, sorted(set(range(2, limit + 1)) - removed)
def main():
steps, primes = sieve_steps(60)
# Independent trial division checks every survivor and removed integer.
direct = [n for n in range(2, 61)
if all(n % d for d in range(2, isqrt(n) + 1))]
assert primes == direct
assert [p for p, _, _ in steps] == [2, 3, 5, 7]
assert [len(new) for _, _, new in steps] == [29, 9, 3, 1]
assert steps[2][2] == {25, 35, 55}
assert steps[3][2] == {49}
assert len(primes) == 17
colors = dict(new='#b7d7ee', old='#e5e5e5', candidate='#ffffff',
prime='#d4ecd8', excluded='#f5f5f5')
fig, axes = plt.subplots(2, 2, figsize=(10.2, 8.4), dpi=110,
facecolor='white')
for i, (ax, (p, old, newly)) in enumerate(zip(axes.flat, steps)):
ax.set(xlim=(0, 10), ylim=(0, 6), aspect='equal')
ax.axis('off')
survivors = 59 - len(old | newly)
ax.set_title(f'Pass p = {p}: start at {p}² = {p*p}\n'
f'{len(newly)} newly marked; {survivors} survivors',
fontsize=12, pad=12)
for n in range(1, 61):
x, y = (n - 1) % 10, 5 - (n - 1) // 10
status = ('excluded' if n == 1 else 'old' if n in old
else 'new' if n in newly else 'prime' if i == 3
else 'candidate')
ax.add_patch(Rectangle((x + .04, y + .04), .92, .92,
facecolor=colors[status],
edgecolor='#2c7545' if n == p else '#b8c0c6',
linewidth=2.2 if n == p else .6))
ax.text(x + .5, y + .5, str(n), ha='center', va='center',
fontsize=11, color='#7b7b7b' if status == 'old' else '#20313c',
fontweight='bold' if status in ('new', 'prime') or n == p
else 'normal')
if n == 1:
ax.plot([x + .18, x + .82], [y + .18, y + .82],
color='#9b9b9b', linewidth=1)
fig.suptitle('Sieve through 60: each pass removes only composite numbers',
fontsize=16, y=.975)
handles = [Patch(facecolor=colors[k], edgecolor='#b8c0c6', label=label)
for k, label in [('new', 'Newly marked'), ('old', 'Marked earlier'),
('candidate', 'Candidate'), ('prime', 'Final prime')]]
fig.legend(handles=handles, loc='lower center', ncol=4, frameon=False,
bbox_to_anchor=(.5, .055), fontsize=10)
fig.text(.5, .025, '1 is excluded. The outlined cell is p; it is never marked. '
'Stop: the next candidate is 11, and 11² > 60.',
ha='center', fontsize=10)
fig.subplots_adjust(left=.035, right=.965, top=.87, bottom=.14,
wspace=.12, hspace=.38)
fig.savefig(Path.cwd() / Path(__file__).with_suffix('.png').name,
facecolor='white', transparent=False)
plt.close(fig)
if __name__ == '__main__':
main()