Простая L-система на плоскости

Архив проекта: 3_fractals.py

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

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

Команда на Python

Символ

Описание

forward(x) [или fd(x)]

F

Движение вперед на x пикселей

left(angle)

+

Поворот влево на угол angle

right(angle)

-

Поворот вправо на угол angle

Тогда команду для рисования одного сегмента кривой Коха можно записать в виде строки:

"F+F--F+F"

Причем, здесь F – это будет одно и то же движение вперед на строго заданное число пикселей, а повороты осуществляться на заданный угол в 60 градусов, так как для формирования кривой Коха нам нужен именно такой угол. У другого фрактала угол может быть другим. В результате, черепашка сначала пройдет вперед, затем, повернет влево на 60 градусов, снова пройдет вперед, повернет вправо на 120 градусов (два символа минус), опять пройдет вперед, повернет влево на 60 градусов и еще пройдет вперед. Получим сгемент ломаной.

Но одной ломаной мало, на каждой новой итерации линейный сегмент также должен превращаться в такую же ломаную. Чтобы это сделать, давайте определим правило: каждый символ F на новой итерации должен заменяться на строку "F+F--F+F":

"F" → "F+F--F+F"

Например, для формирования кривой Коха 2-й итерации черепашка должна будет последовательно выполнить команды:

Мало того, саму первую итерацию также можно сформировать по этому же правилу, взяв за основу обычную прямую:

Вот эта первая предельная прямая в теории L-систем назыается инициатором или аксиомой. Таким образом, имея инициатор и правило формирования, мы получаем возможность генерировать описание фрактала на уровне команд черепашьей графики. Давайте реализуем класс, который бы выполнял описанные действия.

Для начала мы пропишем базовый функционал в классе LSystem2D, который и будет представлять простую L-систему на плоскости:

class LSystem2D:
    def __init__(self, t, axiom, width, length, angle):
        self.axiom = axiom      # инициатор
        self.state = axiom      # строка с набором команд для фрактала (вначале это инициатор)
        self.width = width      # толщина линии рисования
        self.length = length    # длина одного линейного сегмента кривой
        self.angle = angle      # фиксированный угол поворота
        self.t = t              # сама черепашка
        self.t.pensize(self.width)
 
    def draw_turtle(self, start_pos, start_angle):
        # ***************
        turtle.tracer(1, 0)     # форсажный режим для черепашки
        self.t.up()             # черепашка воспаряет над поверхностью (чтобы не было следа)
        self.t.setpos(start_pos)    # начальная стартовая позиция
        self.t.seth(start_angle)    # начальный угол поворота
        self.t.down()               # черепашка опускается на "грешную землю"
        # ***************
 
        for move in self.state:
            if move == 'F':
                self.t.forward(self.length)
            elif move == '+':
                self.t.left(self.angle)
            elif move == '-':
                self.t.right(self.angle)
 
        turtle.done()        # чтобы окно не закрывалось после отрисовки

И воспользуемся им, следующим образом:

# ************** чтобы окно появлялось в левом верхнем углу с размерами 1200x600
width = 1200
height = 600
screen = turtle.Screen()
screen.setup(width, height, 0, 0)
# **************
 
t = turtle.Turtle()
t.ht()          # скрываем черепашку
 
pen_width = 2   # толщина линии рисования (в пикселах)
f_len = 50      # длина одного сегмента прямой (в пикселах)
angle = 60      # фиксированный угол поворота (в градусах)
 
l_sys = LSystem2D(t, "F", pen_width, f_len, angle)
l_sys.draw_turtle( (0, 0), 0)

После запуска должны увидеть прямую линию.

Отлично, начало положено. Осталось реализовать метод, который бы генерировал команды для описания фрактальной кривой согласно заданному правилу (или набору правил). Для этого, мы в конструкторе пропишем свойство, хранящее набор правил в виде словаря:

self.rules = {}  # словарь для хранения правил формирования кривых

А ниже зададим метод, который будет добавлять новое правило в этот словарь:

    def add_rules(self, *rules):
        for key, value in rules:
            self.rules[key] = value

Причем, правила будем передавать в виде списков из двух элементов в формате:

(<что меняем>, <на что меняем>)

Например, для построения кривой Коха, этот список будет таким:

l_sys.add_rules(("F", "F+F--F+F"))

Осталось применить это правило для генерации фрактала. За это будет отвечать метод generate_path:

    def generate_path(self, n_iter):
        for n in range(n_iter):
            for key, value in self.rules.items():
                self.state = self.state.replace(key, value.lower())
 
            self.state = self.state.upper()

В качестве аргумента мы ему передаем число итераций для кривой и, затем, делаем цикл n_iter раз. На каждой итерации во внутреннем цикле перебираем все правила из словаря в формате ключ-значение и в формируемом маршруте (свойстве state) заменяем все найденные ключи на соответствующие значения. Причем, переводим все символы в нижний регистр. Это необходимо, чтобы у нас не было вложенных замен, при использовании нескольких правил. Когда одно правило содержит ключи из другого правила. Поэтому, я реализовал такой трюк – все замены делаются в нижнем регистре, полагая, что ключи всегда будут только в верхнем регистре. После всех замен вновь переводим строку в верхний регистр, как это и должно быть и получаем маршрут для черепашки на текущей итерации. Повторяем эту процедуру n_iter раз и у нас плучается готовая строка с набором необходимых команд.

Теперь у нас все готово для генерации простых фрактальных кривых. Давайте это сделаем. Вначале, конечно же, нарисуем кривую Коха на 4-й итерации. Для этого вызовем метод:

l_sys.generate_path(4)

и установим длину сегмента в 5 пикселей:

f_len = 5

После запуска увидим следующую картину:

Но чем хороша L-система, это то, что теперь у нас есть универсальный инструмент для построения большого числа разных фракталов. Например, чтобы получит снежинку Коха, достаточно инициатор определить в виде равностороннего треугольника:

l_sys = LSystem2D(t, "F--F--F", pen_width, f_len, angle)

Видите, как это просто! Нам не пришлось вносить изменения на уровне команд, а только поменять один параметр.

Что еще можно изобразить такой L-системой? Например, такую кривую:

angle = 90
axiom = "F-F-F-F"
 
l_sys = LSystem2D(t, axiom, pen_width, f_len, angle)
l_sys.add_rules(("F", "F+FF-FF-F-F+F+FF-F-F+F+FF+FF-F"))

С числом итераций 2:

l_sys.generate_path(2)

Увидим следующее построение:

Уже неплохо. Однако, не все кривые можно построить непрерывно перемещая черепашку. Иногда ей нужно перескакивать с места на место. Для этого добавим еще одну команду:

S – переместиться вперед без рисования пути

Для этого в метод draw_turtle() добавим еще одну проверку:

            elif move == 'S':
                self.t.up()
                self.t.forward(self.length)
                self.t.down()

И это все. Теперь можно генерировать еще более сложные фракталы. Например, такой «ковер»:

angle = 90
axiom = "F+F+F+F"
 
l_sys = LSystem2D(t, axiom, pen_width, f_len, angle)
l_sys.add_rules(("F", "F+S-FF+F+FF+FS+FF-S+FF-F-FF-FS-FFF"), ('S', 'SSSSSS'))
l_sys.generate_path(2)

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