Создавайте потрясающие Фрактальные рисунки с помощью Python: Учебное пособие для начинающих и заядлых любителей математики

Создавайте потрясающие Фрактальные рисунки с помощью Python

Вступление

Фразу “Я никогда не видел ничего прекраснее” следует использовать только для фракталов. Конечно, есть “Мона Лиза”, “Звёздная ночь” и “Рождение Венеры”, но я не думаю, что какой-либо художник или человек смог бы создать что-то по-королевски удивительное в виде фракталов.

Слева у нас есть культовый фрактал, множество Мандельброта, обнаруженный в 1979 году, когда не было ни Python, ни программного обеспечения для построения графиков.

Создавайте потрясающие Фрактальные рисунки с помощью Python: Учебное пособие для начинающих и заядлых любителей математики

Множество Мандельброта – это набор комплексных чисел, которые, при нанесении на комплексную плоскость, образуют самоповторяющуюся фигуру, которую мы видим. Каждое число в наборе также может быть начальным для множеств Жюлиа, и вы можете видеть красоты, появляющиеся, когда я перемещаю курсор мыши внутри границы множества Мандельброта.

Прежде чем мы сможем построить множества Мандельброта или Жюлиа (но, поверьте мне, мы это сделаем), нам предстоит проделать большую работу. Если вы здесь просто для того, чтобы посмотреть классные картинки, я настоятельно рекомендую скачать программное обеспечение Fraqtive с открытым исходным кодом (и сходить с ума!), которое я использовал для создания приведённых в статье GIF:

Создавайте потрясающие Фрактальные рисунки с помощью Python: Учебное пособие для начинающих и заядлых любителей математики

Если вы просто хотите отобразить Множество Мандельброта на Python с помощью одной строки кода, вот она (нет, подзаголовок не был кликбейтом):

from PIL import Image

Image.effect_mandelbrot((512, 512), (-3, -2.5, 2, 2.5), 100).show()
Создавайте потрясающие Фрактальные рисунки с помощью Python: Учебное пособие для начинающих и заядлых любителей математики

Но если вы хотите проникнуть в прекрасную кроличью нору фракталов, научиться их отображать и, самое главное, соответствующим образом раскрашивать, тогда продолжайте чтение данной статьи!

В этой статье вы узнаете, как построить базовые (но очень красивые) множества Мандельброта, используя 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:

Создавайте потрясающие Фрактальные рисунки с помощью Python: Учебное пособие для начинающих и заядлых любителей математики

Первый 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
Создавайте потрясающие Фрактальные рисунки с помощью Python: Учебное пособие для начинающих и заядлых любителей математики

Мы видим три типа результатов: когда 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 (предупреждение о спойлере: мы создадим что-то ещё лучше!):

Создавайте потрясающие Фрактальные рисунки с помощью Python: Учебное пособие для начинающих и заядлых любителей математики

Изображение было создано путём раскрашивания всех чисел Мандельброта в чёрный цвет, а нестабильных элементов – в белый. В 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()
Создавайте потрясающие Фрактальные рисунки с помощью Python: Учебное пособие для начинающих и заядлых любителей математики

Что ж, получилось вот такое уродливое Фрактальное изображение. Как насчёт того, чтобы увеличить плотность пикселей до 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()
Создавайте потрясающие Фрактальные рисунки с помощью Python: Учебное пособие для начинающих и заядлых любителей математики

Вот, это больше похоже на желаемый результат! Поздравляем с созданием вашего первого изображения Мандельброта!

Подождите, это ещё не всё!

Несмотря на то, что наш нынешний фрактал по-прежнему выглядит очень круто, он далёк от того искусства, которое я обещал.

Итак, давайте изменим его, сосредоточив внимание не только на цифрах чёрного набора, но и на цифрах по краю. Потому что, глядя на это изображение, мы можем видеть все типы интересных узоров, возникающих вокруг границ:

Создавайте потрясающие Фрактальные рисунки с помощью Python: Учебное пособие для начинающих и заядлых любителей математики

Давайте начнём переделку с организации нашего кода в класс, потому что нам не нужен беспорядок.

Имя класса будет 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,
)
Создавайте потрясающие Фрактальные рисунки с помощью Python: Учебное пособие для начинающих и заядлых любителей математики

Обратите внимание на то, что граничные линии являются самыми яркими и что белые пятна всё ещё появляются там, где множество повторяется. Классно!

Заключение

Наш конечный результат почти можно назвать искусством. Но есть много улучшений, которые мы можем сделать. Первое, что нужно сделать, – это улучшить разрешение изображения за счёт более точного контроля над каждым пикселем. Затем мы должны удалить это раздражающее пустое пространство вокруг изображения (если вы используете в тёмную тему).

Все эти задачи являются недостатками Matplotlib, но в следующей статье мы выведем работу на совершенно новый уровень с помощью Pillow, библиотеки для работы с изображениями на Python.

+1
0
+1
1
+1
4
+1
0
+1
0

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *