Архив проекта: 11_fractals.py
На прошлом
занятии мы с вами реализовали простую систему итерированных функций для
формирования фрактала «ковер Серпинского». Для этого было использовано три
сжимающих отображения для формирования множества точек на текущей итерации:
Так вот, это
множество точек оказывается неизменным, вне зависимости от конфигурации
начального компактного множества :
В пределе, при
числе итераций, стремящихся к бесконечности, мы все равно получим фрактал
«ковер Серпинского» при заданных сжимающих отображениях:
И это важный
момент. Фактически, это означает, что СИФ набором сжимающих функций описывает
конфигурации некоторого неизменного множества точек (в данном случае на
плоскости). И это множество точек есть не что иное как аттрактор (то
есть, предельный маршрут движения, изменения) заданной динамической системы. Аттрактор
еще можно воспринимать как кривую сгущения, к которой стремятся точки
динамической системы. И мы можем в этом легко убедиться.
Давайте для
примера возьмем произвольную точку, допустим с координатами и
применим к ней одно из сжимающих преобразований, выбранное случанйм образом:
Получим новую
точку с
другими координатами. Затем, к ней также применим случано выбранное
отображение для получения третьей точки:
И так далее.
Через n итераций у нас
будет n+1 точка:
Так вот, где бы
мы не взяли начальную точку (с
любыми координатами), множество точек будет последовательно приближаться к
аттрактору, определяемый сжимающими отображениями:
Реализуем этот
простой алгоритм на Python. Для этого мы в класс SIF, который
сформировали на прошлом занятии, добавим метод для получения следующей точки:
def get_next_point(self, pos, pt, scale):
indx = random.randint(0, len(self.T)-1)
pt_new = self.T[indx][0] @ pt + self.T[indx][1] * scale + pos
return (pt_new[0], pt_new[1])
Здесь на вход мы
подаем координату pos точки смещения рисования точек; текущую точку pt; и масштаб
фигуры scale. Затем,
случайным образом выбираем одно из сжимающих отображений и применяем его к
текущей точке pt. На выходе
формируем точку с новыми координатами.
Чтобы в нашем
классе SIF было свойство self.T, в конструкторе
класса пропишем:
class SIF:
def __init__(self, coeffs):
self.coeffs = coeffs
self.T = self.create_funcs()
И
соответствующую строчку уберем из метода create_attractor(). Далее, создадим
экземпляр класса и определим следующие вспомогательные переменные:
sif = SIF(C)
pt = (0, 0)
scale = (200, 200)
pos = (100, 100)
А главный цикл
запишем в виде:
n_iter = 1
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
exit()
if n_iter < 1000:
pygame.draw.circle(sc, BLACK, (round(pt[0]), round(pt[1])), 2)
pt = sif.get_next_point( pos, pt, scale )
n_iter += 1
pygame.display.update()
clock.tick(FPS)
То есть, на
каждой итерации мы рисуем точки на плоскости sc в виде черных
кружочков радиусом в 2 пиксела.
После запуска
программы увидим следующее изображение:
То есть, здесь
начальная точка с координатами (0; 0) сначала последовательно приближалась к
аттрактору динамической системы, а затем, оказавшись в нем, уже практически не
выходит за его пределы. Получается, что набор сжимающих отображений
действительно описывают некоторое замкнутое множество точек, которые описывают,
в данном случае фрактал «ковер Серпинского». Кроме того, мы с вами получили еще
один алгоритм формирования фракталов с помощью СИФ, известный в литературе как рандомизированная
СИФ.
Но в
реализованном нами алгоритме есть один существенный недостаток: равная
вероятность применения того или иного сжимающего отображения. Казалось бы, это
вполне логично – каждое преобразование нужно брать с равной вероятностью. Или,
все же нет?
Давайте
представим, что есть два преобразования, но с разной степенью сжатия:
Тогда размеры
множеств, которые они описывают, будут разными. И если выбирать их с равной
вероятностью, то плотность точек для первого преобразования будет заметно ниже,
чем для второго. Визуально это может сильно бросаться в глаза. Очевидно, здесь
отображение нужно
выбирать чаще, чем .
Но как это сделать?
Нам опять на
помощь приходит математика. Если вспомнить, что геометрический смысл определителя
матрицы (2х2) – это площадь параллелограмма, образованная его векторами:
То мы можем
воспользоваться этой мерой для определения вероятностей выбора сжимающих
отображений:
Давайте
реализуем эту логику в нашей рандомизированной СИФ. Для этого я задам еще один
метод get_probabilities() в классе SIF, который и
будет вычислять эти вероятности:
def get_probabilities(self):
dets = [np.abs(np.linalg.det(t[0])) + 0.1 for t in self.T]
s = sum(dets)
return [d/s for d in dets]
Определитель
вычисляется с помощью функции det() ветки numpy.linalg, затем, все
значения суммируются и вычисляются вероятности.
Далее, нам нужно
выбирать отображения в соответствии с этими вероятностями. Мы это с вами уже
делали, когда рассматривали стохастическую L-систему,
поэтому, я просто приведу метод, который будет выбирать случайным образом
сжимающее отображение:
def get_random_T(self, pr):
p = random.random() # случайное вещественное число в интервале [0; 1]
off = 0
for i, pt in enumerate(pr):
if p < (pt+off):
return self.T[i]
off += pt
return False
А сам метод get_next_point() следует
скорректировать, следующим образом:
def get_next_point(self, pos, pt, scale):
p = self.get_probabilities()
t = self.get_random_T(p)
if not t:
return pt
pt_new = t[0] @ pt + t[1] * scale + pos
return (pt_new[0], pt_new[1])
Все, у нас получилась
полноценная рандомизированная СИФ и при запуске мы увидим тот же аттрактор в
виде ковра Серпинского.