
Лесные пожары – явление столь же древнее, сколь и сама жизнь на суше. Величественные и одновременно ужасающие, они способны за считанные часы превратить гектары зеленого массива в выжженную пустыню, неся угрозу экосистемам, человеческим поселениям и климату планеты. Ежегодно новости пестрят сообщениями о новых очагах возгорания, о борьбе стихии и человека. Но что если мы попытаемся заглянуть в самое сердце этого хаотичного, на первый взгляд, процесса? Что если мы сможем не просто наблюдать, а моделировать, предсказывать и даже экспериментировать с распространением огня, не выходя из-за своего компьютера?
К концу этого материала вы не просто поймете, как из простых локальных правил рождается сложное глобальное поведение, но и напишете код, который позволит вам воочию наблюдать за виртуальным лесным пожаром и изменять его параметры. Мы будем использовать популярные библиотеки, такие как NumPy для эффективных вычислений и Pygame для наглядной визуализации нашего цифрового леса и бушующего в нем пламени.

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

Идея клеточных автоматов не нова. Одними из первых ее исследовали математики Джон фон Нейман и Станислав Улам еще в 1940-х годах, размышляя о возможности создания самовоспроизводящихся машин. Однако настоящую популярность клеточные автоматы обрели благодаря британскому математику Джону Хортону Конвею, который в 1970 году придумал игру "Жизнь" (Conway's Game of Life). Это, пожалуй, самый известный пример клеточного автомата. На простой сетке клетки могут быть либо "живыми", либо "мертвыми", а правила их рождения, выживания и смерти зависят лишь от количества живых соседей. Несмотря на простоту правил, "Жизнь" порождает невероятное разнообразие движущихся, пульсирующих, взаимодействующих и даже самовоспроизводящихся структур, завораживая исследователей и энтузиастов по сей день.
С тех пор клеточные автоматы нашли применение в самых разных областях:
Физика: моделирование роста кристаллов, распространения жидкостей и газов, фазовых переходов.
Биология: изучение роста популяций, распространения эпидемий, формирования паттернов на шкурах животных.
Химия: моделирование химических реакций и диффузии.
Социология и экономика: исследование распространения мнений, транспортных потоков, развития городов.
Информатика и компьютерные игры: генерация процедурного контента (ландшафтов, текстур), создание искусственного интеллекта для игровых персонажей.
Почему же клеточные автоматы так хорошо подходят для моделирования лесных пожаров? Во-первых, лес можно естественным образом представить в виде сетки, где каждая ячейка – это участок определенного типа (например, с деревом или без). Во-вторых, процесс распространения огня по своей природе локален: дерево загорается от соседнего горящего дерева или от искры, прилетевшей с близкого расстояния. Глобальная картина пожара – это результат множества таких локальных взаимодействий. Клеточные автоматы позволяют элегантно и относительно просто описать эти взаимодействия и наблюдать за тем, как из них рождается сложная динамика огненной стихии. Именно этим мы и займемся в следующих разделах, переходя от теории к практике.
Проектируем нашу модель лесного пожара
Прежде чем погрузиться в код, давайте четко определим, как будет устроена наша модель. Это важный этап, который поможет нам структурировать мысли и код.
Состояния клеток: Четыре стихии нашего леса
Каждая ячейка на нашей виртуальной карте леса может находиться в одном из четырех состояний:
ПУСТО(EMPTY), будем обозначать цифрой0: Это участок земли без какой-либо растительности. Он не может гореть и не участвует в распространении огня. Представьте себе голую землю, скалы или водоем.ДЕРЕВО(TREE), обозначаем1: Участок, покрытый деревьями или другой горючей растительностью. Именно эти клетки могут загореться и способствовать распространению пожара.ГОРИТ(FIRE), обозначаем2: Клетка, которая в данный момент охвачена огнем. Это активная фаза пожара, и именно такие клетки являются источником его дальнейшего распространения.СГОРЕЛО(BURNT), обозначаем3: Участок, где пожар уже прошел. Деревья сгорели, остался лишь пепел. Такие клетки больше не могут гореть и не участвуют в распространении огня (по крайней мере, в нашей базовой модели).
Правила перехода: Как огонь танцует по лесу
Теперь самое интересное – правила, по которым клетки будут менять свои состояния. Эти правила будут применяться на каждом шаге нашей симуляции.
Рождение огня (спонтанное возгорание):
Клетка
ДЕРЕВОможет самопроизвольно перейти в состояниеГОРИТс очень маленькой вероятностью (назовем ееPROB_LIGHTNING). Это имитирует случайные события, такие как удар молнии или неосторожное обращение с огнем.
Распространение огня:
Клетка
ДЕРЕВОпереходит в состояниеГОРИТ, если хотя бы одна из ее соседних клеток (мы будем рассматривать 8 соседей – по горизонтали, вертикали и диагоналям) находится в состоянииГОРИТ. Чтобы сделать процесс более реалистичным и менее детерминированным, мы введем вероятность распространения огня (PROB_FIRE_SPREAD). То есть, даже если рядом горит сосед, наше дерево загорится не со 100% вероятностью, а лишь с вероятностьюPROB_FIRE_SPREAD.
Выгорание:
Клетка
ГОРИТне может гореть вечно. Через определенное количество шагов времени (назовем этот параметрFIRE_DURATION) она переходит в состояниеСГОРЕЛО.
Стабильные состояния:
Клетки в состоянии
ПУСТОиСГОРЕЛОне меняют своего состояния в ходе симуляции.
Представление сетки: Наш цифровой лес
Весь наш лес будет представлен в виде двумерной сетки (матрицы). Для эффективной работы с такими структурами в Python идеально подходит библиотека NumPy. Каждая ячейка матрицы будет хранить числовое значение, соответствующее ее состоянию (0, 1, 2 или 3).
Реализация на Python: Шаг за шагом к пылающему лесу

Наконец-то мы добрались до самой интересной части – написания кода! Мы будем использовать Python и несколько популярных библиотек: NumPy для работы с нашей сеткой и Pygame для создания интерактивной визуализации. Если вы предпочитаете Matplotlib, его тоже можно адаптировать для визуализации, но Pygame даст нам больше гибкости для интерактивного управления симуляцией.
Полный код симуляции:
import pygame
import numpy as np
import random
import time
# --- Константы ---
# Состояния клеток
EMPTY = 0
TREE = 1
FIRE = 2
BURNT = 3
# Цвета для визуализации (RGB)
COLOR_EMPTY = (139, 69, 19) # Коричневый (земля)
COLOR_TREE = (0, 100, 0) # Темно-зеленый
COLOR_FIRE = (255, 0, 0) # Красный
COLOR_BURNT = (105, 105, 105) # Серый (пепел)
# Размеры сетки
GRID_WIDTH = 100 # Количество клеток по горизонтали
GRID_HEIGHT = 100 # Количество клеток по вертикали
CELL_SIZE = 6 # Размер каждой клетки в пикселях
# Параметры симуляции
PROB_INITIAL_TREE = 0.65 # Начальная вероятность того, что клетка является деревом
PROB_FIRE_SPREAD = 0.35 # Вероятность распространения огня на соседнее дерево
PROB_LIGHTNING = 0.00005 # Вероятность того, что дерево загорится само (молния)
FIRE_DURATION = 10 # Сколько шагов клетка остается в огне, прежде чем сгорит
# Размеры окна Pygame
WINDOW_WIDTH = GRID_WIDTH * CELL_SIZE
WINDOW_HEIGHT = GRID_HEIGHT * CELL_SIZE
FPS = 10 # Кадров в секунду для симуляции
# --- Основная симуляция ---
def main():
pygame.init()
screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
pygame.display.set_caption("Симуляция Лесного Пожара")
clock = pygame.time.Clock()
# Инициализация сетки
grid = np.zeros((GRID_HEIGHT, GRID_WIDTH), dtype=int)
for r in range(GRID_HEIGHT):
for c in range(GRID_WIDTH):
if random.random() < PROB_INITIAL_TREE:
grid[r, c] = TREE
else:
grid[r, c] = EMPTY
fire_start_times = {} # Словарь для отслеживания времени начала пожара в клетке (r,c) -> time_step
# Установка нескольких начальных очагов возгорания
for _ in range(max(1, int(GRID_WIDTH * GRID_HEIGHT * 0.001))): # Например, 0.1% клеток
while True:
r_fire_init, c_fire_init = random.randint(0, GRID_HEIGHT - 1), random.randint(0, GRID_WIDTH - 1)
if grid[r_fire_init,c_fire_init] == TREE:
grid[r_fire_init,c_fire_init] = FIRE
fire_start_times[(r_fire_init,c_fire_init)] = 0 # Пожар начинается на временном шаге 0
break
running = True
time_step = 0
paused = False
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE: # Пауза/Возобновление
paused = not paused
if event.key == pygame.K_r: # Сброс симуляции
grid = np.zeros((GRID_HEIGHT, GRID_WIDTH), dtype=int)
for r_init in range(GRID_HEIGHT):
for c_init in range(GRID_WIDTH):
if random.random() < PROB_INITIAL_TREE:
grid[r_init, c_init] = TREE
else:
grid[r_init, c_init] = EMPTY
fire_start_times = {}
for _ in range(max(1, int(GRID_WIDTH * GRID_HEIGHT * 0.001))):
while True:
r_f, c_f = random.randint(0, GRID_HEIGHT - 1), random.randint(0, GRID_WIDTH - 1)
if grid[r_f,c_f] == TREE:
grid[r_f,c_f] = FIRE
fire_start_times[(r_f,c_f)] = 0
break
time_step = 0
paused = False # Сбрасываем паузу при рестарте
if not paused and event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1: # Левая кнопка мыши - поджечь дерево
mx, my = pygame.mouse.get_pos()
r_click, c_click = my // CELL_SIZE, mx // CELL_SIZE
if 0 <= r_click < GRID_HEIGHT and 0 <= c_click < GRID_WIDTH:
if grid[r_click,c_click] == TREE:
grid[r_click,c_click] = FIRE
fire_start_times[(r_click,c_click)] = time_step
elif event.button == 3: # Правая кнопка мыши - посадить дерево / потушить (если горит)
mx, my = pygame.mouse.get_pos()
r_click, c_click = my // CELL_SIZE, mx // CELL_SIZE
if 0 <= r_click < GRID_HEIGHT and 0 <= c_click < GRID_WIDTH:
if grid[r_click,c_click] == EMPTY or grid[r_click,c_click] == BURNT:
grid[r_click,c_click] = TREE
elif grid[r_click,c_click] == FIRE: # Тушим огонь
grid[r_click,c_click] = TREE # Восстанавливаем до дерева
if (r_click,c_click) in fire_start_times:
del fire_start_times[(r_click,c_click)] # Удаляем из горящих
if not paused:
new_grid = np.copy(grid) # Работаем с копией, чтобы изменения не влияли на текущий шаг
# Клетки, которые горят в данный момент (копируем ключи, чтобы избежать ошибок при изменении словаря во время итерации)
# current_fire_coords = list(fire_start_times.keys())
# Этот список не используется напрямую в логике ниже, но может быть полезен для отладки или статистики
for r in range(GRID_HEIGHT):
for c in range(GRID_WIDTH):
current_state = grid[r,c]
if current_state == TREE:
# Проверка на удар молнии
if random.random() < PROB_LIGHTNING:
new_grid[r,c] = FIRE
fire_start_times[(r,c)] = time_step
continue # Переходим к следующей клетке, т.к. эта уже загорелась
# Проверка на распространение от соседей (8 соседей)
# Обходим всех 8 соседей
for dr in [-1, 0, 1]:
for dc in [-1, 0, 1]:
if dr == 0 and dc == 0:
continue # Пропускаем саму клетку
nr, nc = r + dr, c + dc # Координаты соседа
# Проверка границ сетки
if 0 <= nr < GRID_HEIGHT and 0 <= nc < GRID_WIDTH:
if grid[nr, nc] == FIRE and random.random() < PROB_FIRE_SPREAD:
new_grid[r,c] = FIRE
fire_start_times[(r,c)] = time_step
break # Дерево загорелось от одного из соседей, выходим из цикла проверки соседей
if new_grid[r,c] == FIRE: # Если дерево загорелось, переходим к следующей клетке в основной сетке
break
elif current_state == FIRE:
# Проверка, не пора ли клетке сгореть
# Используем .get() с значением по умолчанию, чтобы избежать ошибки, если ключ был удален (например, при тушении)
if (r,c) in fire_start_times and (time_step - fire_start_times.get((r,c), time_step)) >= FIRE_DURATION:
new_grid[r,c] = BURNT
# Удаляем из словаря, так как клетка сгорела
if (r,c) in fire_start_times: # Дополнительная проверка перед удалением
del fire_start_times[(r,c)]
grid = new_grid # Обновляем основную сетку результатами текущего шага
time_step += 1
# Отрисовка
screen.fill(COLOR_EMPTY) # Фон по умолчанию - земля (или цвет для EMPTY)
for r in range(GRID_HEIGHT):
for c in range(GRID_WIDTH):
color = COLOR_EMPTY # По умолчанию
if grid[r,c] == TREE:
color = COLOR_TREE
elif grid[r,c] == FIRE:
color = COLOR_FIRE
elif grid[r,c] == BURNT:
color = COLOR_BURNT
pygame.draw.rect(screen, color, (c * CELL_SIZE, r * CELL_SIZE, CELL_SIZE, CELL_SIZE))
pygame.display.flip() # Обновляем весь экран
clock.tick(FPS) # Ограничиваем FPS
pygame.quit()
if __name__ == "__main__":
main()Разбор кода:
Импорт библиотек и константы:
pygame: для графики и интерактивности.numpy: для работы с сеткой (массивом).random: для случайных событий (на��альное распределение деревьев, молнии, распространение огня).time: не используется напрямую в этой версии, но может быть полезен для отладки или задержек.Константы
EMPTY,TREE,FIRE,BURNTопределяют числовые значения для состояний клеток.Цвета
COLOR_...задают RGB-значения для каждого состояния.Размеры сетки (
GRID_WIDTH,GRID_HEIGHT,CELL_SIZE) и параметры симуляции (PROB_INITIAL_TREE,PROB_FIRE_SPREAD,PROB_LIGHTNING,FIRE_DURATION) легко настраиваются.
Функция
main():pygame.init(): инициализирует все модули Pygame.screen = pygame.display.set_mode(...): создает окно для отображения.pygame.display.set_caption(...): устанавливает заголовок окна.clock = pygame.time.Clock(): используется для контроля FPS.
Инициализация сетки:
grid = np.zeros(...): создает NumPy массив, заполненный нулями (состояниеEMPTY).Затем в цикле проходим по каждой клетке и с вероятностью
PROB_INITIAL_TREEустанавливаем ее состояние вTREE.fire_start_times = {}: этот словарь будет хранить время (шаг симуляции), когда каждая конкретная клетка загорелась. Это нужно, чтобы знать, когда клетка должна перейти в состояниеBURNT.Несколько случайных деревьев поджигаются в самом начале, чтобы запустить процесс.
Основной цикл симуляции (
while running):Обработка событий:
pygame.event.get(): получает все события (нажатия клавиш, мыши, закрытие окна).event.type == pygame.QUIT: если пользователь закрыл окно,runningстановитсяFalse, и цикл завершается.event.key == pygame.K_SPACE: пауза/возобновление симуляции.event.key == pygame.K_r: сброс симуляции к начальному состоянию.Обработка кликов мыши (подробнее в следующем разделе).
Логика симуляции (если не на паузе):
new_grid = np.copy(grid): Очень важный момент! Все изменения на текущем шаге должны производиться на копии сетки. Если изменять оригинальную сеткуgridнапрямую, то состояние клетки, обновленное в начале текущего шага, повлияет на расчет состояния ее соседей на этом же шаге, что некорректно. Мы должны рассчитать все новые состояния на основе старых, а затем разом обновить всю сетку.Вложенные циклы
for r in range(GRID_HEIGHT): for c in range(GRID_WIDTH):обходят каждую клетку.Если клетка
TREE:Проверяем на самовозгорание (
PROB_LIGHTNING).Если не загорелась сама, проверяем 8 соседей. Если хотя бы один сосед
FIREи случайное число меньшеPROB_FIRE_SPREAD, то текущая клетка становитсяFIREвnew_grid, и вfire_start_timesзаписывается текущийtime_step.
Если клетка
FIRE:Проверяем, не пора ли ей сгореть. Если разница между текущим
time_stepи временем начала горения этой клетки (fire_start_times.get((r,c), time_step)) больше или равнаFIRE_DURATION, то клетка становитсяBURNTвnew_grid, и удаляется изfire_start_times.
grid = new_grid: после обхода всех клеток обновляем основную сетку.time_step += 1: увеличиваем счетчик времени.
Отрисовка:
screen.fill(COLOR_EMPTY): очищаем экран (заливаем цветом земли).Вложенные циклы обходят сетку, и для каждой клетки рисуется прямоугольник (
pygame.draw.rect) соответствующего цвета.pygame.display.flip(): обновляет содержимое всего экрана, чтобы показать нарисованное.clock.tick(FPS): делает паузу, чтобы поддерживать заданный FPS.
pygame.quit(): корректно завершает работу Pygame при выходе из цикла.
Наша симуляция уже работает, но настоящее веселье начинается, когда мы можем с ней взаимодействовать! Pygame предоставляет для этого все необходимые инструменты.
Управление симуляцией:
В основном цикле, в блоке обработки событий, мы уже добавили:
Пауза/Возобновление (Пробел):
if event.type == pygame.KEYDOWN: if event.key == pygame.K_SPACE: paused = not pausedЭто позволяет остановить симуляцию в любой момент, чтобы рассмотреть текущее состояние, а затем продолжить.
Сброс симуляции (Клавиша R):
if event.key == pygame.K_r: # ... (код для реинициализации сетки и fire_start_times) time_step = 0 paused = False # Сбрасываем паузу при рестартеЭто возвращает лес к первоначальному случайному состоянию с новыми очагами возгорания.
Взаимодействие с пользователем (мышью):
Мы можем позволить пользователю самому влиять на ход пожара:
Поджечь дерево (Левая кнопка мыши):
if not paused and event.type == pygame.MOUSEBUTTONDOWN: if event.button == 1: # Левая кнопка мыши mx, my = pygame.mouse.get_pos() r_click, c_click = my // CELL_SIZE, mx // CELL_SIZE # Преобразуем координаты мыши в индексы сетки if 0 <= r_click < GRID_HEIGHT and 0 <= c_click < GRID_WIDTH: # Проверка, что клик внутри сетки if grid[r_click,c_click] == TREE: # Если кликнули по дереву grid[r_click,c_click] = FIRE # Поджигаем его fire_start_times[(r_click,c_click)] = time_step # Запоминаем время возгоранияПосадить дерево / Потушить огонь (Правая кнопка мыши):
elif event.button == 3: # Правая кнопка мыши mx, my = pygame.mouse.get_pos() r_click, c_click = my // CELL_SIZE, mx // CELL_SIZE if 0 <= r_click < GRID_HEIGHT and 0 <= c_click < GRID_WIDTH: if grid[r_click,c_click] == EMPTY or grid[r_click,c_click] == BURNT: # Если пусто или сгорело grid[r_click,c_click] = TREE # Сажаем дерево elif grid[r_click,c_click] == FIRE: # Если горит grid[r_click,c_click] = TREE # Тушим (превращаем обратно в дерево) if (r_click,c_click) in fire_start_times: del fire_start_times[(r_click,c_click)] # Удаляем из списка горящих
Теперь вы можете активно вмешиваться в процесс: создавать новые очаги пожара, чтобы посмотреть, как они будут распространяться, или, наоборот, пытаться остановить огонь, превращая горящие участки обратно в деревья (или в сгоревшие, если хотите более реалистичного тушения).
Улучшение графики (идеи):
Хотя наши цветные квадраты функциональны, визуализацию можно сделать и поприятнее:
Более естественные цвета: Поэкспериментируйте с оттенками зеленого для деревьев, оранжевого и желтого для огня, темно-серого для пепла.
Простые спрайты: Вместо заливки цветом можно рисовать небольшие иконки (спрайты) для каждого состояния. Pygame позволяет легко загружать и отображать изображения.
Статистика на экране: Можно выводить на экран количество горящих клеток, процент сгоревшей территории и т.д. Для этого понадобится использовать функции Pygame для рендеринга текста (
pygame.font).
Обсуждение возможностей и ограничений модели:
Наша модель, несмотря на свою относительную простоту, позволяет увидеть и понять некоторые ключевые аспекты распространения лесных пожаров:
Пороговый характер: Если плотность деревьев слишком мала или вероятность распространения огня низкая, пожар может быстро затухнуть сам по себе. Если же эти параметры выше определенного порога, огонь будет распространяться лавинообразно.
Роль связности: Огонь распространяется только по связанным участкам леса. Большие пустые пространства могут остановить пожар.
Формирование фронта: Можно наблюдать, как образуются и движутся фронты пламени.
Конечно, наша модель имеет и ограничения: мы не учитывали ветер, рельеф, влажность, разные типы растительности, возможность тушения и многие другие факторы, влияющие на реальные лесные пожары. Однако прелесть моделирования в том, что мы всегда можем усложнить нашу модель, добавляя новые параметры и правила.
Заключение
Мы с вами проделали большой путь: от знакомства с концепцией клеточных автоматов до создания собственной интерактивной симуляции лесного пожара на Python. Надеюсь, это путешествие было для вас не только познавательным, но и увлекательным. Вы увидели, как из простых правил могут рождаться сложные и динамичные системы, и научились воплощать эти идеи в коде.
Моделирование – это мощный инструмент для понимания окружающего нас мира. Оно позволяет нам экспериментировать, проверять гипотезы и получать наглядное представление о процессах, которые в реальности могут быть слишком масштабными, опасными или труднодоступными для прямого изучения. Наша симуляция лесного пожара – это лишь один из бесчисленных примеров того, как программирование помогает нам исследовать и открывать новое.

