File:Collatz Fractal.jpg
Generally, this JPEG version should be used when displaying the file from Commons, in order to reduce the file size of thumbnail images.
However, any edits to the image should be based on the original PNG version in order to prevent generation loss, and both versions should be updated. Do not make edits based on this version. here for more information.
Source code
See Python code |
---|
import enum
import itertools
import time
from math import floor, ceil
import numba as nb
import numpy as np
import matplotlib
from PIL import Image, PyAccess
# Amount of times to print the total progress
PROGRESS_STEPS: int = 20
# Set to `True` to plot the shortcut version of the fractal
SHORTCUT: bool = True
# Make all integers critical points
FIX_CRITICAL_POINTS: bool = True
# Width of the image in pixels and aspect ratio
RESOLUTION: int = 1920*1080//4
ASPECT_RATIO: float = 21/9 if FIX_CRITICAL_POINTS else 16/9
# Value of the center pixel
CENTER: complex = 0 + 0j
# Value range of the real part (width of the horizontal axis)
RE_RANGE: float = 10 if FIX_CRITICAL_POINTS else 5
# Show grid lines for integer real and imaginary parts
SHOW_GRID: bool = False
GRID_COLOR: tuple[int, int, int] = (125, 125, 125)
# Matplotlib named colormap
COLORMAP_NAME: str = 'inferno'
# Plot range of the axes
X_MIN = CENTER.real - RE_RANGE/2 # min Re(z)
X_MAX = CENTER.real + RE_RANGE/2 # max Re(z)
Y_MIN = CENTER.imag - RE_RANGE/(2*ASPECT_RATIO) # min Im(z)
Y_MAX = CENTER.imag + RE_RANGE/(2*ASPECT_RATIO) # max Im(z)
x_range = X_MAX - X_MIN
y_range = Y_MAX - Y_MIN
pixels_per_unit = np.sqrt(RESOLUTION/(x_range*y_range))
# Width and height of the image in pixels
WIDTH = round(pixels_per_unit*x_range)
HEIGHT = round(pixels_per_unit*y_range)
# Maximum iterations for the divergence test (recommended >= 60)
MAX_ITER: int = 60
# Max value of Re(z) and Im(z) for which the recursion doesn't overflow
CUTOFF_RE = 7.564545572282618e+153
CUTOFF_IM = 112.10398935569289 if SHORTCUT else 111.95836403625282
# Smallest positive real fixed point
INNER_FIXED_POINT = 0.277733766171606 if SHORTCUT else 0.150108511304474
# Precompute the colormap
CMAP_LEN: int = 2000
cmap_mpl = matplotlib.colormaps[COLORMAP_NAME]
# Start away from 0 (discard black values for the 'inferno' colormap)
# Matplotlib's colormaps have 256 discrete color points
n_cmap = round(256*0.98)
CMAP = [cmap_mpl(k/256) for k in range(256 - n_cmap, 256)]
# Interpolate
x = np.linspace(0, 1, num=CMAP_LEN)
xp = np.linspace(0, 1, num=n_cmap)
c0, c1, c2 = tuple(np.interp(x, xp, [c[k] for c in CMAP]) for k in range(3))
CMAP = []
for x0, x1, x2 in zip(c0, c1, c2):
CMAP.append(tuple(round(255*x) for x in (x0, x1, x2)))
class DivType(enum.Enum):
"""Divergence type."""
CONVERGED = -1 # Converged
MAX_ITER = 0 # Maximum iterations reached
CUTOFF_RE = 1 # Diverged by exceeding the real part cutoff
CUTOFF_IM = 2 # Diverged by exceeding the imaginary part cutoff
@nb.jit(nb.float64(nb.float64, nb.int64), nopython=True)
def smooth(x, k=1):
"""Recursive exponential smoothing function."""
y = np.expm1(np.pi*x)/np.expm1(np.pi)
if k <= 1:
return y
return smooth(y, np.fmin(6, k - 1))
@nb.jit(nb.float64(nb.float64, nb.float64), nopython=True)
def get_delta(x, cutoff):
"""Get the fractional part of the smoothed divergence count."""
nu = np.log(np.abs(x)/cutoff)/(np.pi*cutoff - np.log(cutoff))
nu = np.fmax(0, np.fmin(nu, 1))
return smooth(1 - nu, 2)
@nb.jit(
nb.types.containers.Tuple((
nb.float64,
nb.types.EnumMember(DivType, nb.int64)
))(nb.complex128),
nopython=True
)
def divergence_count(z):
"""Return a smoothed divergence count and the type of divergence."""
z_fix = 0 + 0j
for k in range(MAX_ITER):
c = np.cos(np.pi*z)
if SHORTCUT:
if FIX_CRITICAL_POINTS:
z_fix = (0.5 - c)*np.sin(np.pi*z)/np.pi
z = 0.25 + z - (0.25 + 0.5*z)*c + z_fix
else: # Regular
if FIX_CRITICAL_POINTS:
z_fix = (1.25 - 1.75*c)*np.sin(np.pi*z)/np.pi
z = 0.5 + 1.75*z - (0.5 + 1.25*z)*c + z_fix
if np.abs(z.imag) > CUTOFF_IM:
# Diverged by exceeding the imaginary part cutoff
return k + get_delta(z.imag, CUTOFF_IM), DivType.CUTOFF_IM
if np.abs(z.real) > CUTOFF_RE:
# Diverged by exceeding the real part cutoff
return k + get_delta(z.real, CUTOFF_RE), DivType.CUTOFF_RE
if np.abs(z) < INNER_FIXED_POINT:
# Converged to a fixed point
return -1, DivType.CONVERGED
# Maximum iterations reached
return -1, DivType.MAX_ITER
@nb.jit(nb.float64(nb.float64), nopython=True)
def cyclic_map(g):
"""A continuous function that cycles back and forth from 0 to 1."""
# This can be any continuous function.
# Log scale removes high-frequency color cycles.
freq_div = 12
g = np.log1p(np.fmax(0, (g - 1)/freq_div))
# Beyond this value for float64, decimals are truncated
if g >= 2**51:
return -1
# Normalize and cycle
# g += 0.5 # phase from 0 to 1
return 1 - np.abs(2*(g - np.floor(g)) - 1)
@nb.jit(nb.complex128(nb.types.containers.UniTuple(nb.float64, 2)),
nopython=True)
def pixel_to_z(p):
"""Convert pixel coordinates to its corresponding complex value."""
re = X_MIN + (X_MAX - X_MIN)*p[0]/WIDTH
im = Y_MAX - (Y_MAX - Y_MIN)*p[1]/HEIGHT
return re + 1j*im
class Progress:
"""Simple progress check helper class."""
def __init__(self, n: int, steps: int = 10):
self.n = n
self.k = 0
self.steps = steps
self.step = 1
self.progress = 0
def check(self) -> bool:
self.k += 1
self.progress = self.k/self.n
if self.steps*self.k >= self.step*self.n:
self.step += 1
return True
return self.progress == 1
def create_image():
img = Image.new('RGB', (WIDTH, HEIGHT))
pix = img.load()
pix: PyAccess
n_pix = WIDTH*HEIGHT
prog = Progress(n_pix, steps=PROGRESS_STEPS)
for p in itertools.product(range(WIDTH), range(HEIGHT)):
c = pixel_to_z(p)
g, div_type = divergence_count(c)
if g >= 0:
pix[p] = CMAP[round(cyclic_map(g)*(CMAP_LEN - 1))]
else:
# Color of the interior of the fractal
pix[p] = (0, 0, 0)
if prog.check():
print(f'{prog.progress:<7.1%}')
if SHOW_GRID:
for x in range(ceil(X_MIN), floor(X_MAX) + 1):
px = round((x - X_MIN)*(WIDTH - 1)/(X_MAX - X_MIN))
for py in range(HEIGHT):
pix[(px, py)] = GRID_COLOR
for y in range(ceil(Y_MIN), floor(Y_MAX) + 1):
py = round((Y_MAX - y)*(HEIGHT - 1)/(Y_MAX - Y_MIN))
for px in range(WIDTH):
pix[(px, py)] = GRID_COLOR
return img
img = create_image()
strtime = time.strftime('%Y%m%d-%H%M%S')
fractal_type = 'Shortcut' if SHORTCUT else 'Regular'
filename = f'Collatz_{fractal_type}_{strtime}.png'
img.save(filename)
|
Licensing
I, the copyright holder of this work, hereby publish it under the following license:
This file is made available under the Creative Commons CC0 1.0 Universal Public Domain Dedication. | |
The person who associated a work with this deed has dedicated the work to the public domain by waiving all of their rights to the work worldwide under copyright law, including all related and neighboring rights, to the extent allowed by law. You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission.
|