Waldbrandsimulation in Python (Py5) – Stage 1

Python
Simulation
Modellbildung
Py5
Processing
Autor:in

Jörg Kantel

Veröffentlichungsdatum

23. September 2023

Ich habe es getan: Ich habe mir die hier vorgestellte Waldbrand-Simulation von Al Sweigart und meine eigene, in Processing.py implementierte Version geschnappt und daraus eine – wie ich hoffe – verbesserte Fassung in Py5 programmiert.

Zwei grundsätzliche Änderungen gegenüber dem Programm von Al Sweigart habe ich vorgenommen: Zum einen habe ich die Randbedingungen vereinfacht, indem ich die Felder an den Rändern grundsätzlich leer gelassen habe (der Wald hört also nicht an den Rändern, sondern ein Zeile oder Spalte davor auf). Das ändert nichts an dem Verhalten der Simulation (der Wald ist einfach nur je eine Zeile oben und unten und je eine Spalte rechts und links kleiner), erspart der Programmiererin oder dem Programmierer aber eine Menge Arbeit bei der Behandlung der Ränder: Wenn die Abfragen immer nur von range(1, N_ROWS-1) respektive von range(1, N_COLS-1) reichen, können sie nie die Dimensionen eines zweidimendionalen Arrays sprengen.

Die zweite Änderung bedarf schon einer größeren Diskussion: Statt der Moore-Nachbarschaft, die von Al Sweigart verwendet wurde und bei der alle acht Nachbarfelder einer Zelle abgefragt werden, habe ich die Von-Neumann-Nachbarschaft verwendet, bei der nur die vier Zellen, die eine Kante mit der Basisfläche gemeinsam haben, als Nachbarn berücksichtigt werden. Diese Nachbarschaft habe ich aus pragmatischen Gründen gewählt, da die Abfrage von vier statt von acht Nachbarn den Code halbiert. Und ich denke, daß man die Wahl damit begründen kann, daß die diagonal entfernten Felder, die nur eine Ecke mit der Basiszelle gemeinsam haben, weiter entfernt von der Basiszelle liegen und daher weniger der Brandgefahr ausgesetzt sind. Ich befinde mich da in guter Gesellschaft, da auch in der einschlägigen Literatur fast immer die Von-Neumann-Nachbarschaft Verwendung findet1. Außerdem habe ich keine grundsätzliche Änderung im Verhalten der Simulation bei Verwendung der Moore-Nachbarschaft feststellen können.

Das Modell hat in seiner einfachsten Form drei Paramter, an denen geschraubt werden kann und die Einfluß auf den Verlaut der Simulation haben: Es sind mit a die Wahrscheinlichkeit, mit der ein einem Feld ein Baum (nach-) wächst, b die Wahrscheinlichkeit, mit der ein Baum (zum Beispiel durch Blitzeinschlag) Feuer fängt und s die Wahrscheinlichkeit, mit welcher Dichte beim Start das Feld mit Bäumen besetzt ist. Sowohl bei Sweigart wie auch bei Scholz finden sich weiter Ideen, wie die Siumlation erweitert und ausgebaut werden kann.

Doch jetzt erst einmal das komplette Programm, damit Ihr die Simulation nachlesen, nachprogrammieren oder mit eigenen Ideen erweitern könnt:

## Forest Fire Simulation
from random import randint, uniform

TS = 16       # Tilesize
N_ROWS, N_COLS = 40, 30
PLOT_WIDTH = 720
PLOT_HEIGHT = N_COLS*TS
SCREEN_WIDTH, SCREEN_HEIGHT = N_ROWS*TS, N_COLS*TS   # 640x480 Pixel

empty = 0
tree = 1
burning = 20
w = h = TS

a = 4.0        # Wachstumswahrscheinlichkeit in Prozent
b = 0.2        # Wahrscheinlichkeit Blitzeinschlag in Prozent
s = 50         # Startbepflanzung in Prozent

grid = []
newgrid = []

trees = []
fires = []

def setup():
    global tree_image, fire_image, no_trees, no_fire
    size(SCREEN_WIDTH, SCREEN_HEIGHT)
    window_title("🌲 Forest Fire Simulation 🔥")
    window_move(1300, 30)
    tree_image = load_image("data/tree.png")
    fire_image = load_image("data/fire.png")
    no_trees = no_fire = 0
    create_new_forest()
    newgrid[:] = grid[:]
    frame_rate(1)
    print("Start: Bäume = ", no_trees, " Brände = ", no_fire)
    trees.append(no_trees)
    fires.append(no_fire)
    
def draw():
    background(210, 180, 140)
    print("Runde: ", frame_count, " Bäume = ", no_trees, " Brände = ", no_fire)
    display_plot()
    trees.append(no_trees)
    fires.append(no_fire)
    display_forest()
    calc_next()
    if frame_count == 30:
        no_loop()

def display_plot():
    pass

def create_new_forest():
    global no_trees
    for x in range(N_ROWS):
        grid.append([])
        newgrid.append([])
        for y in range(N_COLS):
            # Erstbepflanzung Wald
            if ((x > 0) and (y > 0) and (x < N_ROWS - 1)
                and (y < N_COLS - 1) and randint(0, 100) <= s):
                grid[x].append(tree)
                no_trees += 1
            else:
                # Ränder bleiben leer
                grid[x].append(empty)

def display_forest():
    for i in range(N_ROWS):
        for j in range(N_COLS):
            if grid[i][j] == tree:
                image(tree_image, i*w, j*h, w, h)
            elif grid[i][j] == burning:
                image(fire_image, i*w, j*h, w, h)

def calc_next():
    global no_trees, no_fire
    newgrid[:] = grid[:]
    # Next Generation, Randfelder ignorieren
    for i in range(1, N_ROWS - 1):
        for j in range(1, N_COLS - 1):
            if grid[i][j] == burning:
                newgrid[i][j] = empty
                no_fire -= 1
                # Nachbarbaum anzünden (von-Neumann-Nachbarschaft)
                if grid[i-1][j] == tree:
                    newgrid[i-1][j] = burning
                    no_trees -= 1
                    no_fire += 1
                if grid[i][j-1] == tree:
                    newgrid[i][j-1] = burning
                    no_trees -= 1
                    no_fire += 1
                if grid[i][j+1] == tree:
                    newgrid[i][j+1] = burning
                    no_trees -= 1
                    no_fire += 1
                if grid[i+1][j] == tree:
                    newgrid[i+1][j] = burning
                    no_trees -= 1
                    no_fire += 1
            ## Neuer Baum?
            elif grid[i][j] == empty:
                if uniform(0, 100) < a:
                    newgrid[i][j] = tree
                    no_trees += 1
            # Schlägt ein Blitz ein?
            if grid[i][j] == tree:
                if uniform(0, 100) < b:
                    newgrid[i][j] = burning
                    no_fire += 1
    grid[:] = newgrid[:]

In dem Skript sind schon Slots offengehalten, um einen Plot des Simulationsverlaufes zu erstellen. Leider weigert sich die Matplotlib hartnäckig, mit Py5 zusammenzuarbeiten. Daher ist meine Idee, FPlotter (2), meine in Processing.py implementierte »Matplotlib für Arme« wieder zu reaktivieren und in dieser Simulation einzusetzen. Das wird vermutlich mein nächster Schritt sein.

Ansionsten gibt es den Quellcode wie immer in meinem GitHub-Repositorium und die Bilder habe ich den freien (CC-BY 4.0) Twemojis von Twitter entnommen und mit einem Graphikprogramm meines Vertrauens auf 16x16 Pixel verkleinert.

Verwendete Literatur:

  • Heinz-Otto Peitgens, Hartmut Jürgens und Dietmar Saupe: Bausteine des Chaos – Fraktale, Berlin-Heidelberg-Stuttgart (Springer, Klett-Cotta) 1992
  • Daniel Scholz: Pixelspiele – Modellieren und Simulieren mit zellulären Automaten, Berlin-Heidelberg (Springer) 2014
  • Al Sweigart: The Big Book of Small Python Projects – 81 Easy Practice Programs, San Francisco (No Starch Press) 2021

Fußnoten

  1. Vergleiche Peitgen, Jürgens, Saupe: Bausteine des Chaos – Fraktale, Berlin-Heidelberg-Stuttgart (Klett-Cotta/Springer) 1992, Seiten 424ff. oder Scholz: Pixelspiele, Berlin-Heidelberg (Springer) 2014, Seiten 19ff.↩︎