Создавайте потрясающие Фрактальные рисунки с помощью Python: Учебное пособие для начинающих и заядлых любителей математики
Создавайте потрясающие Фрактальные рисунки с помощью Python
Вступление
Фразу “Я никогда не видел ничего прекраснее” следует использовать только для фракталов. Конечно, есть “Мона Лиза”, “Звёздная ночь” и “Рождение Венеры”, но я не думаю, что какой-либо художник или человек смог бы создать что-то по-королевски удивительное в виде фракталов.
Слева у нас есть культовый фрактал, множество Мандельброта, обнаруженный в 1979 году, когда не было ни Python, ни программного обеспечения для построения графиков.
Множество Мандельброта – это набор комплексных чисел, которые, при нанесении на комплексную плоскость, образуют самоповторяющуюся фигуру, которую мы видим. Каждое число в наборе также может быть начальным для множеств Жюлиа, и вы можете видеть красоты, появляющиеся, когда я перемещаю курсор мыши внутри границы множества Мандельброта.
Прежде чем мы сможем построить множества Мандельброта или Жюлиа (но, поверьте мне, мы это сделаем), нам предстоит проделать большую работу. Если вы здесь просто для того, чтобы посмотреть классные картинки, я настоятельно рекомендую скачать программное обеспечение Fraqtive с открытым исходным кодом (и сходить с ума!), которое я использовал для создания приведённых в статье GIF:
Если вы просто хотите отобразить Множество Мандельброта на Python с помощью одной строки кода, вот она (нет, подзаголовок не был кликбейтом):
from PIL import Image
Image.effect_mandelbrot((512, 512), (-3, -2.5, 2, 2.5), 100).show()
Но если вы хотите проникнуть в прекрасную кроличью нору фракталов, научиться их отображать и, самое главное, соответствующим образом раскрашивать, тогда продолжайте чтение данной статьи!
В этой статье вы узнаете, как построить базовые (но очень красивые) множества Мандельброта, используя Matplotlib и NumPy.
Давайте начинать!
Комплексные числа в Python
Программисты на Python не каждый день имеют дела со сложными числами. Поскольку в этом руководстве мы будем много работать с ними, этот раздел послужит в качестве основы.
Вы можете создать мнимую часть комплексного числа, добавив литерал j к целым числам или числам с плавающей точкой:
num1 = 2 + 1j
num2 = 12.3 + 23.1j
type(num1)
complex
Если вас смущает вид мнимых чисел, представленных буквой j вместо i (привет, математики), вы можете воспользоваться встроенной функцией complex
:
2 + 3j == complex(2, 3)
True
После создания вы можете получить доступ к действительным и мнимым компонентам комплексных чисел с атрибутами real
и imag
:
num1.real
2.0
num2.imag
23.1
Другим важным свойством комплексных чисел для целей этой статьи является их абсолютное значение. Абсолютное значение или величина комплексного числа измеряет его расстояние от начала координат (0, 0) в комплексной плоскости. Оно определяется как квадратный корень из суммы его действительной и мнимой частей (спасибо тебе, Пифагор).
abs(1 + 3.14j)
3.295390720385065
Этого нам будет достаточно, чтобы создать несколько потрясающих вещей. Давайте продолжим!
Простая формула, грандиозное Множество
Наше путешествие начинается с выяснения, принадлежит ли некоторое комплексное число c
множеству Мандельброта, что на удивление просто реализовать. Всё, что нам нужно сделать, это использовать приведённую ниже формулу и создать последовательность значений z
:
Первый z
всегда равен 0, как определено выше. Последующие элементы находятся путем возведения в квадрат предыдущего z и добавления c
к результату.
Давайте реализуем этот процесс на Python. Мы определим функцию sequence
, которая возвращает первые n
элементов для данного c
:
def sequence(c, n=7) -> list:
z_list = list()
z = 0
for _ in range(n):
z = z ** 2 + c
z_list.append(z)
return z_list
Теперь мы возьмём функцию на тест-драйв для набора чисел:
import pandas as pd
df = pd.DataFrame()
df['element'] = [f"z_{i}" for i in range(7)]
# Random numbers
cs = [0, 1, -1, 2, 0.25, -.1]
for c in cs:
df[f"c={c}"] = sequence(c)
df
Мы видим три типа результатов: когда c
равно либо 1, либо 2, последовательность неограниченна (расходится в бесконечность) по мере её роста. Когда оно равно -1, оно перемещается взад и вперед между 0 и -1. Что касается 0.25 и -0.1, они остаются маленькими или ограниченными.
Итак, кому из этих пятерых повезло войти в Множество Мандельброта?
Стабильность чисел
Наш процесс отбора очень прост — если c
расходится в последовательности до бесконечности, то её нет в Множестве Мандельброта. На жаргоне фракталов этот c
называется нестабильным. Или давайте отбросим отрицательность — данное комплексное число c
стабильно, если соответствующая ему последовательность Z
остаётся ограниченной.
Теперь мы должны выяснить, на сколько членов Z следует обратить внимание, прежде чем классифицировать их как стабильные или нестабильные. Это количество итераций найти неочевидно, поскольку формула чувствительна даже к мельчайшим изменениям в c
.
Но, к счастью, люди изучали множества достаточно долго, чтобы знать, что все Множества Мандельброта остаются ограниченными в радиусе двух. Это означает, что мы можем выполнить несколько десятков итераций, и числа, которые остаются относительно небольшими или ниже 2, вероятно, находятся в Множестве Мандельброта.
Итак, давайте создадим новую функцию is_stable
, используя эту логику, которая возвращает True
, если число входит в Множество Мандельброта:
def is_stable(c, n_iterations=20):
z = 0
for _ in range(n_iterations):
z = z ** 2 + c
if abs(z) > 2:
return False
return True
В теле этой логической функции мы устанавливаем z
равным 0 и запускаем её по алгоритму в цикле, управляемом n_iterations
. На каждой итерации мы проверяем величину z
, чтобы мы могли завершить цикл, если она превысит 2 на ранней стадии, и не тратить время на выполнение остальных итераций.
Последний оператор return
выполняется только в том случае, если z
меньше 2 после всех итераций. Давайте проверим несколько цифр:
is_stable(1)
False
is_stable(0.2)
True
is_stable(0.26)
True
is_stable(0.26, n_iterations=30)
False
Обратите внимание, как увеличение n_iterations
до 30 изменяет стабильность 0.26. Как правило, значения, близкие к границе фракталов, требуют большего количества итераций для более точной классификации и создания более детализированных визуальных эффектов.
Как построить Множество Мандельброта в Matplotlib
Конечная цель данной статьи – создать Фрактальный рисунок в Matplotib (предупреждение о спойлере: мы создадим что-то ещё лучше!):
Изображение было создано путём раскрашивания всех чисел Мандельброта в чёрный цвет, а нестабильных элементов – в белый. В Matplotlib оттенки серого имеют 256 оттенков или находятся в диапазоне от 0 до 255, 0 – полностью белый, а 255 – черный, как смоль. Но вы можете нормализовать этот диапазон на 0 и 1 так, чтобы 0 было белым, а 1 – черным.
Эта нормализация пригодится нам. Мы можем создать 2D-массив комплексных чисел и запустить нашу функцию is_stable
для каждого элемента. Результирующий массив будет содержать единицы для чисел Мандельброта и 0 для нестабильных. Когда мы выводим этот массив в виде изображения — вуаля, мы получаем желаемый чёрно-белый визуал.
Давайте перейдём к этому. Сначала мы создаем функцию, которая генерирует матрицу возможных значений, по которым мы будем выполнять итерации:
import numpy as np
def candidate_values(xmin, xmax, ymin, ymax, pixel_density):
# Generate a 2D grid of real and imaginary values
real = np.linspace(xmin, xmax, num=int((xmax-xmin) * pixel_density))
imag = np.linspace(ymin, ymax, num=int((ymax-ymin) * pixel_density))
# Cross each row of `xx` with each column of `yy` to create a grid of values
xx, yy = np.meshgrid(real, imag)
# Combine the real and imaginary parts into complex numbers
matrix = xx + 1j * yy
return matrix
Мы будем использовать функцию np.linspace
для создания равномерно распределённых чисел в пределах диапазона. Параметр pixel_density
динамически задаёт количество пикселей на единицу.
Например, матрица с горизонтальным диапазоном (-2, 0), вертикальным диапазоном (-1,2, 1,2) и pixel_density
, равной 1, имела бы форму (2, 2). Это означает, что наше результирующее изображение Мандельброта было бы 2 пикселя в ширину и 2 пикселя в высоту, что заставило бы Бенуа Мандельброта перевернуться в могиле.
c = candidate_values(-2, 0, -1.2, 1.2, 1)
c.shape
(2, 2)
Итак, нам лучше использовать более высокую плотность, например 25:
c = candidate_values(-2, 0, -1.2, 1.2, 25)
c.shape
(60, 50)
Теперь, чтобы запустить нашу функцию is_stable
для каждого элемента c
, мы векторизуем её с помощью np.vectorize
и вызываем её с 20 итерациями:
c = candidate_values(-2, 0.7, -1.2, 1.2, pixel_density=25)
mandelbrot_mask = np.vectorize(is_stable)(c, n_iterations=20)
mandelbrot_mask.shape
(60, 67)
Мы называем результирующий массив mandelbrot_mask
, поскольку он возвращает True
(1) для каждого числа Мандельброта. Чтобы отобразить этот массив, мы используем функцию imshow
Matplpotlib с двоичной цветовой картой. Это сделает изображение чёрно-белым.
import matplotlib.pyplot as plt
plt.imshow(mandelbrot_mask, cmap="binary")
# Turn off the axes and use tight layout
plt.axis("off")
plt.tight_layout()
Что ж, получилось вот такое уродливое Фрактальное изображение. Как насчёт того, чтобы увеличить плотность пикселей до 1024 и количество итераций до 30?
c = candidate_values(-2, 0.7, -1.2, 1.2, pixel_density=1024)
mandelbrot_mask = np.vectorize(is_stable)(c, n_iterations=30)
plt.imshow(mandelbrot_mask, cmap="binary")
plt.gca().set_aspect("equal")
plt.axis("off")
plt.tight_layout()
Вот, это больше похоже на желаемый результат! Поздравляем с созданием вашего первого изображения Мандельброта!
Подождите, это ещё не всё!
Несмотря на то, что наш нынешний фрактал по-прежнему выглядит очень круто, он далёк от того искусства, которое я обещал.
Итак, давайте изменим его, сосредоточив внимание не только на цифрах чёрного набора, но и на цифрах по краю. Потому что, глядя на это изображение, мы можем видеть все типы интересных узоров, возникающих вокруг границ:
Давайте начнём переделку с организации нашего кода в класс, потому что нам не нужен беспорядок.
Имя класса будет Mandelbrot
, и мы будем использовать классы данных, чтобы нам не пришлось создавать конструктор __init__
, как пещерному человеку:
from dataclasses import dataclass
@dataclass
class Mandelbrot: # Inspired by the Real Python article shared above
n_iterations: int
def is_stable(self, c: complex) -> bool:
z = 0
for _ in range(self.n_iterations):
z = z ** 2 + c
if abs(z) > 2:
return False
return True
Классу требуется только инициализация параметра max_iteration
. Мы также добавляем функцию is_stable
в качестве метода класса:
mandelbrot = Mandelbrot(n_iterations=30)
mandelbrot.is_stable(0.1)
True
mandelbrot.is_stable(1 + 4.4j)
False
До сих пор мы раскрашивали числа Мандельброта только в чёрный цвет, а остальные – в белый. Но если мы хотим оживить края набора, мы должны придумать логику, чтобы раскрасить нестабильные элементы в другой цвет.
Один из способов, которым мы можем это сделать, – определить, за сколько итераций комплексное число становится нестабильным. Некоторые из них очень быстро станут нестабильными, но другим могут потребоваться сотни или тысячи итераций. В целом, однако, числа, близкие к краю фрактала, менее нестабильны (требуют большего количества итераций), чем те, которые находятся далеко.
Используя эту информацию, мы можем придать каждому пикселю (комплексному числу) различную глубину цвета в зависимости от итерации, которую они завершают. Это называется алгоритмом подсчета побегов. Давайте реализуем это в нашем классе:
@dataclass
class Mandelbrot:
max_iterations: int
def escape_count(self, c: complex) -> int:
z = 0
for iteration in range(self.max_iterations):
z = z ** 2 + c
if abs(z) > 2:
return iteration
return self.max_iterations
Сначала мы меняем n_iterations
на max_iterations
, так как это имеет больше смысла. Затем мы создаём метод
escape_count, который:
- если
c
нестабилен, возвращает итерацию, в которой он превышает величину 2 - если
c
стабилен, возвращает максимальное количество итераций
mandelbrot = Mandelbrot(max_iterations=50)
mandelbrot.escape_count(-0.1) # stable
50
mandelbrot.escape_count(0.26) # unstable
29
Теперь мы создаём другой метод для измерения стабильности на основе количества итераций:
@dataclass
class Mandelbrot:
max_iterations: int
def escape_count(self, c: complex) -> int:
z = 0
for i in range(self.max_iterations):
z = z ** 2 + c
if abs(z) > 2:
return i
return self.max_iterations
def stability(self, c: complex) -> float:
return self.escape_count(c) / self.max_iterations
Метод стабильности возвращает значение от 0 до 1, которое позже мы можем использовать для определения глубины цвета. Только числа Мандельброта вернут max_iterations
, поэтому они будут отмечены 1. Числам, близким к краю, потребуется больше времени, чтобы стать нестабильными, поэтому они будут иметь значения, всё более близкие к 1.
С помощью этой логики мы можем вернуть нашу функцию is_stable
, но сделать её намного короче:
@dataclass
class Mandelbrot:
max_iterations: int
def escape_count(self, c: complex) -> int:
z = 0
for i in range(self.max_iterations):
z = z ** 2 + c
if abs(z) > 2:
return i
return self.max_iterations
def stability(self, c: complex) -> float:
return self.escape_count(c) / self.max_iterations
def is_stable(self, c: complex) -> bool:
# Return True only when stability is 1
return self.stability(c) == 1
mandelbrot = Mandelbrot(max_iterations=50)
mandelbrot.stability(-.1)
1.0
mandelbrot.is_stable(-.1)
True
mandelbrot.stability(2)
0.02
mandelbrot.is_stable(2)
False
Теперь мы создаём окончательный метод для построения множества с помощью Matplotlib:
@dataclass
class Mandelbrot:
max_iterations: int
# ... The rest of the code from above
@staticmethod
def candidate_values(xmin, xmax, ymin, ymax, pixel_density):
real = np.linspace(xmin, xmax, num=int((xmax-xmin) * pixel_density))
imag = np.linspace(ymin, ymax, num=int((ymax-ymin) * pixel_density))
xx, yy = np.meshgrid(real, imag)
matrix = xx + 1j * yy
return matrix
def plot(self, xmin, xmax, ymin, ymax, pixel_density=64, cmap="gray_r"):
c = Mandelbrot.candidate_values(xmin, xmax, ymin, ymax, pixel_density)
# Apply `stability` over all elements of `c`
c = np.vectorize(self.stability)(c)
plt.imshow(c, cmap=cmap, extent=[0, 1, 0, 1])
plt.gca().set_aspect("equal")
plt.axis('off')
plt.tight_layout()
В plot
мы применяем метод stability
ко всем элементам c
, поэтому результирующая матрица сохраняет глубину цвета в каждой ячейке. Когда мы наносим эту матрицу на цветовую карту в перевёрнутых оттенках серого (так, чтобы числа Мандельброта оставались чёрными), мы получаем следующее изображение:
mandelbrot = Mandelbrot(max_iterations=30)
mandelbrot.plot(
xmin=-2, xmax=0.5,
ymin=-1.5, ymax=1.5,
pixel_density=1024,
)
Обратите внимание на то, что граничные линии являются самыми яркими и что белые пятна всё ещё появляются там, где множество повторяется. Классно!
Заключение
Наш конечный результат почти можно назвать искусством. Но есть много улучшений, которые мы можем сделать. Первое, что нужно сделать, – это улучшить разрешение изображения за счёт более точного контроля над каждым пикселем. Затем мы должны удалить это раздражающее пустое пространство вокруг изображения (если вы используете в тёмную тему).
Все эти задачи являются недостатками Matplotlib, но в следующей статье мы выведем работу на совершенно новый уровень с помощью Pillow, библиотеки для работы с изображениями на Python.