0. Как я познакомился с клеточными автоматами
В начале 2022 года я, обычный студент 4 курса с направления «Радиофизика», вместо того, чтобы постигать труды по ТОЭ и радиоэлектронике, смотрел YouTube в поисках интересного контента. Меня очень увлекала занимательная математика и головоломки, поэтому я был подписан на множество каналов про околонаучные темы, в том числе по программированию. Мне в ленте попалось потрясающее видео с канала «Onigiri», я всем советую ознакомится с ним, чтобы лучше погрузиться в контекст. Оно сделано очень качественно и очаровывает своим простым повествованием, при этом то, что происходит на экране очень увлекает!
Посмотрите на досуге, а пока, чтобы не отвлекаться, я вам кратко опишу о чем же рассказывают в этом видео.
1. Автоматы которые не стреляют
Представьте перед собой тетрадный лист в клетку. И для начала возьмем простой карандаш и закрасим несколько десятков клеток на этом листе. Дальше немного поиграем, правила очень просты:
Посмотрим на каждую клетку, у каждой вокруг есть соседи:
Если клетка закрашена, то мы ее оставим закрашенной только если у нее 2 или 3 закрашенных соседа. В остальных случаях, к примеру, как на рисунке, клетку нужно стереть.
Если клетка не закрашена, то мы ее закрасим только если рядом у нее 3 закрашенных соседа.
Повторим для каждой клетки (лучше брать небольшой листочек, а то можно быстро устать) и увидим, что наш рисунок уже отличается от начального.
Таких проходов можно сделать сколько угодно и каждый раз наш рисунок на листке будет меняться, то есть наша картинка эволюционирует по обозначенным нами правилам.
Можно сказать, что клетка «рождается», когда соседа 3, «выживает», когда соседей 2 или 3, а в остальных случаях либо «умирает», либо вообще «не рождается». Поэтому такие правила для игры назвали B3/S23, B – Born в переводе с английского «рождение», а S – Survival, что значит «выживание».
В такую игру можно надолго залипнуть на листочке, но спустя время появляется желание побаловаться с этим на компьютере, так как уж больно долго делать все это от руки, поэтому надо автоматизировать!
2. Простенький код с пояснениями
Если вы никогда не программировали, то это отличный повод начать, хоть для совсем начинающих это будет сложно, но я все равно постараюсь написать подробно про каждую строку кода, благо их надо совсем чуть-чуть. Я буду использовать язык Python просто потому, что умею писать только на нем! :)
Итак, нам понадобиться сделать анимацию нашего тетрадного листа, будто компьютер быстро закрашивает и стирает клетки по нужным правилам. Для такой анимации я буду использовать библиотеку matplotlib. Библиотека matplotlib это просто код, который написали другие добрые и умные люди, он содержит функции, которые помогают строить различные графики, в том числе и анимировать их. Также, я хочу в начале случайно закрасить какие-то клетки, значит мне нужна функция для генерации случайных значений. Для этих целей я всегда использую библиотеку numpy, там есть такая функция.
Подключаем наши библиотеки:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
Заметьте, что я импортирую два модуля из библиотеки matplotlib это pyplot, который и будет строить график, а также animation, который будет его анимировать. Вы спросите, откуда я знаю, что мне нужны такие модули. Все модули и их функции описаны в документации к библиотеке:
https://matplotlib.org/stable/users/index.html
Дальше я введу переменные для нашей программы:
N = 50
ON = 255
OFF = 0
vals = [ON, OFF]
N = 50: Это размерность игровой сетки (нашего листка в клетку). Я для простоты сделаю квадратный листок N*N, то есть у нас будет 50 клеток в длину и 50 в ширину.
ON = 255, OFF = 0: Эти две строки определяют состояния клеток. «Живая» клетка, она же ON, будет иметь значение 255, а «мертвая», она же OFF, будет иметь значение 0. Вы спросите, что за странные значения, ведь было бы логичнее выбрать 1 и 0. 1 – клетка жива, 0 – мертва. Все дело в библиотеке matplotlib. Значение 255 будет отображаться как белый цвет, а 0 - как черный.
vals = [ON, OFF]: Это просто список, содержащий два возможных состояния клетки - ON и OFF. Этот список используется при инициализации игровой сетки случайными значениями. То есть мы просто возьмем функцию для случайных значений и попросим разбросать случайным образом значения из этого списка по полю N*N.
Сделать это проще, чем кажется. Введем переменную grid, что значит "сетка" с английского. В нее мы положим результат выполнения той самой функции, которая будет случайным образом выбирать значения из списка vals для заполнения нашего поля N*N: np.random.choice:
grid = np.random.choice(vals, N*N, p=[0.2, 0.8])
Но вы видите, что я добавил ещё p=[0.2, 0.8]. Параметр p=[0.2, 0.8] задаёт вероятности выбора соответствующих состояний. То есть, есть 20% вероятности, что клетка будет в состоянии ON (живая), и 80% вероятности, что клетка будет в состоянии OFF (мертвая). Я выбрал именно такие значения просто так, можно экспериментировать с этим параметром, как и с величиной сетки. Это может быть полезно для создания более интересных и разнообразных начальных конфигураций. Если бы вы установили p=[0.5, 0.5], то у вас было бы равное количество "живых" и "мертвых" клеток в начальной конфигурации. Или, если бы вы установили p=[0.9, 0.1], большинство клеток были бы "живыми".
Друзья, также есть ещё один интересный момент. Функция np.random.choice вернет нам одномерный массив длиной N*N, а мы то помним, что нам нужен двумерный клеточный листок, значит надо превратить этот одномерный массив в двумерный массив (то есть в сетку, листок). Для этого просто допишем к нашей строке ещё одну функцию: reshape(N, N):
grid = np.random.choice(vals, N*N, p=[0.2, 0.8]).reshape(N, N)
Теперь у нас есть клеточный листок с изначальными закрашенными («живыми») и не закрашенными («мертвыми») клетками. Настал самый интересный момент, как заставить их эволюционировать по нашим правилам? Давайте для этого напишем собственную функцию, хотя я уверен, что есть библиотеки, в которых это уже реализовано, но также совсем неинтересно, верно? :)
Мы сделаем такую функцию, которую будем вызывать каждый раз, когда нужно изменить состояние наших клеток на листке. То есть мы вызываем функцию, она обновляет сетку по вышеописанным правилам, а затем выводит на экран. И так мы делаем как бы «мультик» кадр за кадром выводя новое состояние сетки.
Определим нашу функцию:
def update(data):
Дальше скажем интерпретатору, что хотим использовать уже имеющуюся у нас сетку, а она у нас лежит в глобальной переменной grid (то есть вне какой-либо функции):
global grid
Создаем копию текущего состояния сетки. Мы будем изменять newGrid, а не исходную сетку grid, чтобы не влиять на наши расчеты в процессе обновления:
newGrid = grid.copy()
Дальше нам нужно пройтись по каждой клетке в каждой строке нашей сетки, для этого будем использовать простейший двойной цикл, где i номер строки из N, а j номер клетки из N:
for i in range(N):
for j in range(N):
Для каждой клетки мы должны знать кол-во живых соседей вокруг нее, для этого введем переменную total, которая и будет отражать это число:
total = (grid[i, (j-1)%N] + grid[i, (j+1)%N] +
grid[(i-1)%N, j] + grid[(i+1)%N, j] +
grid[(i-1)%N, (j-1)%N] + grid[(i-1)%N, (j+1)%N] +
grid[(i+1)%N, (j-1)%N] + grid[(i+1)%N, (j+1)%N])/255
Не смотрите на кажущуюся сложность этого выражения, на самом деле все очень просто:
Первое, что мы учли в этом выражении – листок прямоугольный, а значит краевые клетки не всегда имеют 8 соседних, как мы договаривались. И чтобы исправить это мы свернем наш листок так, чтобы у краевых клеток всегда были соседи. Поверхность, где края нашей сетки будут соединяться с противоположными, создавая бесшовное пространство без границ – тор. Мы можем свернуть наш прямоугольник в тор!
Сначала прямоугольник может свернуться в цилиндр (две параллельные стороны прямоугольника смыкаются). Затем цилиндр может свернуться в тор (оставшиеся стороны цилиндра смыкаются).
-
Это геометрическое превращение мы проделываем с помощью всего одной операции – %N. Оператор % в Python - это оператор модуля, который возвращает остаток от деления. Оператор %N возвращает остаток от деления на N, где N - размер сетки. Но как это работает?
Давайте разберем это на конкретном примере. Предположим, что у нас есть сетка размером 5x5 (N = 5). Если мы взглянем на клетку с индексом (0,0) (верхний левый угол), то у нас есть соседи вверху и слева. Но, так как это край сетки, кажется, что соседей нет:
Мы хотим обратиться к ее соседу слева, чтобы проверить «мертв» он или «жив», индекс этого соседа будет (0, -1). Однако в Python отрицательные индексы интерпретируются как обращение к элементам с конца массива, поэтому (0, -1) будет ссылаться на последний элемент в нулевой строке, что является желаемым поведением в этом случае. Но что будет в таком случае?
Мы хотим обратиться к соседу справа, чтобы также его проверить, но он уже будет иметь индекс (0, 5) (так как grid[i, (j+1)]), что говорит о том, что мы вышли за пределы цикла, потому что для сетки размером N цикл будет проходить значения от 0 до N-1. Вспомним:
for i in range(N):
for j in range(N):
-
И что делать? Python даст ошибку IndexError, а проверить соседей справа как-то надо. Тут и приходит на помощь %N:
Мы делим наш j-ый индекс на N = 5 и смотрим на остаток от деления (grid[i, (j+1)%N), в этом случае он ноль – 5/5 = 1, остаток = 0. И получаем индекс (0,0), именно тот, который нам нужен!
То есть при обращении к соседям справа или внизу от клетки на правом или нижнем краю сетки, мы получим индексы, превышающие размер сетки. В этих случаях, (i+1)%N или (j+1)%N вернут нам индекс 0, что также является желаемым поведением. Это наконец-то делает индексацию "циклической", или "тороидальной", как мы обсуждали ранее. Мы свернули прямоугольник в тор! Второе, что мы учли, что все «живые» клетки обозначаются значением 255, поэтому чтобы получить число «живых» клеток в переменной total нужно будет поделить полученную сумму на 255.
После того, как мы посчитали всех «живых» соседей нашей клетки, напишем простое логическое выражение, которое вводило бы наши правила игры в программу:
if grid[i, j] == ON:
if (total < 2) or (total > 3):
newGrid[i, j] = OFF
else:
if total == 3:
newGrid[i, j] = ON
Разберем построчно:
if grid[i, j] == ON
1) Если клетка жива
if (total < 2) or (total > 3)
2) И если у нее меньше 2 или больше 3 «живых» соседей
newGrid[i, j] = OFF
3) То в новом «кадре» нашей анимации мы ее «убьем»
else:
4) Иначе, если клетка «мертва»
if total == 3
5) И если у нее ровно 3 «живых» соседа
newGrid[i, j] = ON
6) То в новом «кадре» нашей анимации мы ее «возрадим».
Вот и готова наша игра! Теперь за пределами цикла, после того как мы обработали все клетки, мы заменяем старую сетку новой:
grid = newGrid
И последняя волшебная команда:
mat.set_data(grid)
Эта операция обновляет данные в объекте mat, который используется для визуализации сетки на графике. Это обновление заставит matplotlib отобразить новое состояние сетки при следующем кадре анимации.
А теперь сделаем так, чтобы наша функция вернула этот объект:
return [mat]
Эта строка возвращает список из одного элемента - объекта mat, который был обновлен на предыдущей строке. Это нужно для функции FuncAnimation из matplotlib.animation, которая ожидает, что функция обновления вернет список объектов, которые были изменены.
Именно из-за FuncAnimation мы определили функцию в начале с входной переменной data. Коротко говоря, data включается в определение функции update для совместимости с FuncAnimation, даже если оно не используется.
Функция для эволюции наших клеток на листочке готова, переходим к созданию анимации!
fig, ax = plt.subplots()
Эта строка создает новый график, используя функцию subplots из matplotlib.pyplot. Эта функция возвращает два объекта: Figure (здесь сохраняется как fig) и Axes (здесь сохраняется как ax). Figure - это весь график, а Axes - это область графика, где данные отображаются.
Как вы понимаете, теперь надо передать в Axes наши данные после обновления сетки – grid:
mat = ax.matshow(grid)
Мы применяем к Axes функцию matshow() и в нее передаем нашу новую сетку grid, тем самым говорим, чтобы функция matshow() преобразовала наш массив так, чтобы он превратился в объект AxesImage, который можно отобразить уже в Axes. И мы уже готовый объект для отображения устанавливаем в переменную mat.
График готов, кадр анимации тоже, теперь просто поместим все это в одну функцию:
ani = animation.FuncAnimation(fig, update, interval=50, save_count=50)
Как мы уже указывали это функция FuncAnimation из модуля animation. Туда мы передаем наш график fig, функцию для обновления анимации update и два параметра:
Параметр interval=50 указывает, что между кадрами анимации должен быть интервал в 50 миллисекунд, а save_count=50 указывает, что должно быть сохранено 50 кадров анимации. Увеличение interval замедлит вашу анимацию, так как будет больше времени между кадрами, в то время как уменьшение interval ускорит вашу анимацию.
Увеличение save_count может привести к большему использованию памяти, но позволит сохранить больше кадров, если вы решите сохранить анимацию в файл.
И последняя главная команда, чтобы посмотреть результат наших стараний на экране компьютера:
plt.show()
Gif-ку сделал вот так:
ani = animation.FuncAnimation(fig, update, interval=40, save_count=1000)
ani.save('game_of_life.gif', writer='pillow', fps=25)
Только учтите важный момент -- для создания GIF вам также потребуется установить ImageMagick или Pillow. Я использую Pillow, так как его легко установить через командную строку Python:
pip install pillow
3. Игра "Жизнь".
Приглядитесь внимательнее, мы с вами с нуля сделали целую симуляцию! 30 строк кода, а на экране возникает целая жизнь! Наша игра на листке бумаги переросла в колонии клеток, которые размножаются, перемещаются, убивают друг друга. Здесь много интересных структур, которые возникают в процесс эволюции нашей «популяции» клеток, включая некоторые, которые могут двигаться по сетке ("космические корабли") и другие, которые генерируют новые клетки в регулярных интервалах ("генераторы"). Если вам интересно, то я обязательно расскажу о них максимально подробно в следующих статьях.
То, что мы с вами получили называется клеточный автомат! В общем случае клеточный автомат – дискретная динамическая математическая модель, состоящая из регулярной решетки, но размерность решетки, форма, правила эволюции, соседи, все это многообразие параметров не установлено заранее. И вы даже не представляете, что в себе несет такая простая математическая модель.
Мы запрограммировали всего один достаточно простой частный случай клеточного автомата, который называется «Игра «Жизнь». Эту игру придумал английский математик Джон Конвей в 1970 году, он с детства вдохновлялся трудами Джона Фон Неймана, который придумал концепт клеточного автомата. И хоть гений Фон Неймана дал жизнь клеточным автоматам, его правила были достаточны сложны для обывателей, а вот Конвей изначально ставил себе цель сделать максимально простой по правилам клеточный автомат, но при этом с нетривиальным поведением, также он хотел добиться полноты по Тьюрингу. Простыми словами это значит, что на таком клеточном автомате можно реализовать любую функцию, даже сам клеточный автомат (да, игра в жизнь внутри игры в жизнь). Кстати, такие умельцы нашлись:
Полнота по Тьюрингу дает возможность воссоздать даже маленький компьютер:
Весь секрет заключается в том, чтобы найти нужную начальную конфигурацию клеточного автомата (а мы начальную конфигурацию делали случайным образом). Надо ли говорить, что это очень сложная задача!
"Жизнь" оказалась удачным выбором и быстро стала популярной из-за своей простоты и интересных визуальных эффектов.
4. Продолжение следует...
Друзья, я большой фанат клеточных автоматов и если вам понравилась моя статья про игру «Жизнь», то напишите об этом, и я обязательно расскажу ещё много интересного про эти удивительные математические структуры.
Полный код клеточного автомата вы сможете найти на GitHub
Спасибо за ваше внимание! До новых встреч!
Александр Глебов
Комментарии (11)
phenik
26.05.2023 05:03+1То, что мы с вами получили называется клеточный автомат! В общем случае клеточный автомат – дискретная динамическая математическая модель, состоящая из регулярной решетки, но размерность решетки, форма, правила эволюции, соседи, все это многообразие параметров не установлено заранее. И вы даже не представляете, что в себе несет такая простая математическая модель.
Игра Жизнь де-факто является модельной системой исследования эмерджентности. Поэтому с ее помощью можно пробовать моделировать саму реальность и разумную жизнь в ней. Конечно в меру собственных представлений об этом)
aleksandrGlebov Автор
26.05.2023 05:03Здравствуйте! Вы абсолютно правы! "Игра Жизнь" действительно является хорошим примером эмерджентности, которая является ключевым концептом в многих областях, включая физику, биологию, социологию и компьютерные науки. Эти свойства или поведения не могут быть предсказаны или объяснены только на основе знания о свойствах элементов на более низком уровне. И это, правда, интереснейшее явление!
Спасибо вам за материалы!
truefreewill
26.05.2023 05:03-3Очень интересная тема. Дж. Конвей, конечно, гений. Хотя совсем недавно на Хабре его обвиняли в лженауке.
Если это и жизнь, то очень странная. Для рождения нужно три родителя. Двух недостаточно.
Если соседей больше трех, то клетка погибает.Это каннибализм?
Для жизни обязательно нужна пища. Т.е. клетки должны быть, как минимум, двух видов: живая клетка и пища. Живая клетка должна обладать органами чувств, например, чувствовать пищу на расстоянии и двигаться к ней, если голодна.
Погибать живая клетка должна от голода, т.е. от отсутствия пищи в течении нескольких циклов.
-
У живых клеток должны быть эмоции. Им должно "нравиться" соседство трех клеток, и "не нравиться" столпотворение и одиночество.
и т.д. Наращивая подобные правила можно дойти и до разумных клеток. Только это будут уже не одиночные клетки, а устойчивые комплексы клеток, которые содержат в себе модель внешнего мира.
Что-то подобное я , кажется, встречал, Нужно искать. Но википедию посмотреть автору нужно, уж точно.
aleksandrGlebov Автор
26.05.2023 05:03Вы абсолютно правы. Несмотря на то что "Игра Жизнь" получила большую известность и широкое признание, Джон Конвей внес вклад в множество других областей математики, которые могли быть менее заметны для широкой публики. Однако, именно за эти работы он получил уважение в академических кругах.
Сам Конвей, как известно, был несколько неоднозначным относительно своего отношения к "Игре Жизнь". С одной стороны, он признавал ее простоту и элегантность, и рассматривал ее как инструмент для демонстрации ключевых концепций в математике и компьютерных науках. С другой стороны, он также выразил опасения, что известность "Игры Жизнь" может затметь его другие достижения.
А их было много!
1) Конвей известен своими работами по конечным простым группам. Он доказал существование некоторых из них и сформулировал "список Конвея" этих групп.2) Работа в области теории игр и введение в математику сюрреальных чисел.
3) Огромный вклад в нотацию для кристаллографии и в изучение многогранников и замощений.
В целом, в русскоязычной википедии есть отличная статья про научные достижения Конвея, так что всех сомневающихся можно отправлять читать ее.Survtur
26.05.2023 05:03+1У вас стиль ответов похож на кое-что...
aleksandrGlebov Автор
26.05.2023 05:03Приветствую вас! Вы абсолютно правы, заметив, что мои высказывания часто содержат выражения и фразы, напоминающие те, что можно найти в Википедии и других интернет-ресурсах. Действительно, я часто опираюсь на эти источники! Я привык заимствовать оттуда отдельные части текста, которые, по моему мнению, кратко и ясно выражают мои мысли. Однако, как я убежден, это не уменьшает качество моих текстов и комментариев.
Vsevo10d
26.05.2023 05:03+3Да все, короче, надо возрождать традицию английских клубов джентльменов, где все новости и вопросы обсуждаются лично под сигары и скотч. Любой электронный собеседник в каментах или автор на ресурсе может теперь оказаться чем-то вот этим самым.
aleksandrGlebov Автор
26.05.2023 05:03+1А что, интересная идея! Я бы с радостью присоединился к такому клубу! В моем городе есть клуб парламентских дебатов с личными встречами, и реализация подобного в формате технической тематики звучит весьма привлекательно.
aleksandrGlebov Автор
26.05.2023 05:03+2Давайте про комментарии к игре "Жизнь".
Важно помнить, что "Игра Жизнь" является абстрактной и упрощенной моделью. Она не моделирует реальность или разумную жизнь в полном смысле этих слов. Вместо этого она предоставляет интересный инструмент для исследования некоторых общих принципов и идей, таких как эмерджентность, саморепликация и динамика популяций.
Поэтому в контексте моделирования реальности или разумной жизни "Игра Жизнь" должна рассматриваться прежде всего как инструмент обучения и исследования, а не как точное представление реальности. Как и все модели, она имеет свои ограничения и упрощения и может быть использована только для иллюстрации определенных концепций, а не для предсказания конкретных реальных сценариев.
Правила игры могут казаться странными, действительно, но...
Одиночество: В игре "Жизнь" клетка умирает, если у нее меньше двух соседей, что можно интерпретировать как метафору одиночества или недостатка разнообразия партнеров для репликации. Это отражает идею, что во многих системах общение и "социализация" критичны для выживания.
Перенаселенность: Клетка в игре "Жизнь" также умирает, если у нее есть более трех соседей, что можно интерпретировать как перенаселенность или борьбу за ресурсы. Это отражает реальные проблемы перенаселенности и конкуренции за ресурсы, с которыми сталкиваются многие виды в живой природе.
Добавление правил: Действительно, базовые правила игры "Жизнь" довольно просты, и, добавляя новые, можно создавать более сложные и реалистичные модели. Однако следует помнить, что дополнительные правила могут усложнить систему и сделать ее менее предсказуемой. Это отражает принцип, что более сложные системы часто становятся более трудными для анализа.
Vova-Grib
Классная статья. Реализация на Python неожиданная и поэтому интересная. Ждём продолжения!
aleksandrGlebov Автор
Спасибо большое за теплые слова! Постараюсь сделать вторую часть не менее интересной!