I was watching 3Blue1Brown's YouTube video "Why do prime numbers make these spirals?", and it inspired me to look for some patterns myself. So I made some Python code as follows below. How it works is that each nonnegative integer is placed along a spiral, with x(n) = n * cos(n) and y(n) = n * sin(n). Only numbers that are palindromes in the chosen base (original question is base 10) are displayed on the spiral, every other number is hidden.
#!/usr/bin/env python3
from typing import Generator
import argparse
import math
from PIL import Image, ImageDraw
def to_base(n: int, base: int) -> list[int]:
"""Converts a number to a base other than 10
Maximum base: 36
"""
if base < 2 or base > 36:
raise ValueError("Base must be greater than 1 and less than 36")
digits = []
while n >= base:
n, m = divmod(n, base)
digits.append(m)
digits.append(n)
return digits
def palindromes(base: int = 10) -> Generator[int, None, None]:
def is_palindrome(n: int):
sn = to_base(n, base)
return sn == sn[::-1]
n = 1
while True:
if is_palindrome(n):
yield n
n += 1
def spiral_coords(
n: float, x_origin: float = 0.0, y_origin: float = 0.0, scale: float = 1.0
) -> tuple[float, float]:
x = n * math.cos(n)
y = n * math.sin(n)
return x * scale + x_origin, y * scale + y_origin
def fill_spiral(
gen: Generator[int, None, None],
height: int,
width: int,
radius: float = 1.0,
scale: float = 1.0,
):
"""Fills an Image of size `(height, width)` with zeroes.
Then, fills each element of `gen` in on the spiral
"""
x_origin = width / 2
y_origin = height / 2
img = Image.new("1", (width, height), 0)
draw = ImageDraw.Draw(img)
for n in gen:
x, y = spiral_coords(n, x_origin, y_origin, scale)
# if either x or y is in bounds, draw circle
if 0 <= x < width or 0 <= y < height:
top_left = (x - radius, y - radius)
bottom_right = (x + radius, y + radius)
draw.ellipse([top_left, bottom_right], 1)
else:
break
return img
def main():
parser = argparse.ArgumentParser(description="Create an image of the prime spiral")
parser.add_argument("height", type=int, help="Image height")
parser.add_argument("width", type=int, help="Image width")
parser.add_argument(
"--radius", type=float, help="Radius of each point, default 1", default=1.0
)
parser.add_argument("--base", type=int, help="Number base, default 10", default=10)
parser.add_argument(
"--scale",
type=float,
help="Zoom scale. Larger means more zoomed in, smaller is more zoomed out. Default 1",
default=1.0,
)
args = parser.parse_args()
spiral = fill_spiral(
palindromes(args.base), args.height, args.width, args.radius, args.scale
)
spiral.show()
if __name__ == "__main__":
main()
And I ran the code as: ./main.py 10000 10000 --radius 10, receiving
.
Outside of the dense cloud of palindromes in the middle, there is nothing but empty space and these arcs of palindromes. Some observations:
- The spiral runs counterclockwise but these arcs run clockwise
- Each arc is five palindromes long
- Each arc appears to be placed along one of four clockwise spiral arms out of the central cloud
If anyone could make a larger image including more data points that would also be welcome. My image viewer apparently likes to crap out if I make the image any larger than 10000x10000 pixels, so I'm not getting as much data as I would like.
Edit: a comment asked about the number base. The displayed image is in base 10, but changing the base creates different patterns. For instance, base 5
, and base 16
All your points are on the Archimedean spiral that comes from polar plotting $(t,t)$. At the scale you are plotting that spiral is too tight to notice. I plotted the four digit palindromes in a spreadsheet and show the result below. I believe your series of five come from these. The cause of the groups of five is that the palindromes come in groups of $10$ at intervals of $110$. The first group of $10$ starts at $1001$ and ends at $1991$. The next starts at $2002$ and ends at $2992$ and so on. It turns out that $110$ radians is $\frac {110}{2\pi} \approx 17.507$ full turns, so an increment of $220$ results in just over $35$ full turns and the next angle is $0.014$ full turns or $5^\circ$. That explains the groups of $5$. The jump to the next group is $1001$, which represents about $159.314$ full turns. It will be about $1/3$ of a turn around from the previous group.