這一節的標題是
Elementary Cellular Automata
因為方格子標題字數限制,所以沒完整顯現
最最簡單的CA長什麼樣子?網格、狀態、鄰域是構成CA的三個關鍵要素。最簡單的網格是由一串細胞所構成的1D網格;除了只有單一一個狀態的情況不論,因為一成不變的細胞沒啥好討論的,細胞可以擁有的狀態,最少會有2個;在1D網格中的細胞,其最簡單、單純的鄰域定義方式,是包括細胞本身,以及跟它相鄰的左、右兩個鄰居,共三個細胞。Wolfram把具備這些特點的1D CA叫做基礎CA (elementary CA)。所以,假設細胞的狀態以0、1來表示,基礎CA會長這樣:
在CA的世界中,時間是以世代(generation)來表示。一開始的時候時間是0,這時候的CA是第0世代。接下來,利用某些事先定義好的規則,根據第0世代細胞的狀態,可以算出第1世代細胞的狀態;接著再根據第1世代細胞的狀態,算出第2世代細胞的狀態;如此不斷地迭代下去,就可以得到不同世代的CA。
CA的迭代規則,可以是數學計算式。例如,假設細胞b在第t世代時的狀態是bt,而其左右兩個鄰居細胞的狀態分別是at與ct。如果我們設定細胞b在下一世代時的狀態bt+1是透過下式計算出來的
bt+1 = (at + bt + ct) mod 2
那當
at = 0
bt = 1
ct = 0
時,細胞b在第t+1世代的狀態就會是
bt+1 = (0 + 1 + 0) mod 2 = 1
有別於數學計算式,Wolfram則是採用另一種方式來設定基礎CA的迭代規則;這些規則就統稱為規則集(ruleset)。
Wolfram的基礎CA迭代規則集設定方式很簡單,就只是針對鄰域細胞狀態值的所有可能排列組合,一一指定一個新的狀態值而已。對於某個細胞而言,它的鄰域共有3個細胞,所以鄰域細胞狀態值的所有可能排列組合共有8種,也就是
111 110 101 100 011 010 001 000
針對這8個不同的排列組合,分別指定對應的新狀態值,例如
0 1 0 1 1 0 1 0
這樣子就完成了規則集的制定。當某個細胞的鄰域細胞狀態值為010時,從規則集中可以找到對應的新狀態值是0,所以在下一世代時,這個細胞的狀態值就是0。畫張圖來看會更清楚一些:
制定好基礎CA的迭代規則集之後,準備一個初代的CA,就可以利用規則集讓這個CA不斷地迭代產生新世代。這個初代的CA,依照Wolfram的標準做法,會是一個除了中間細胞的狀態值是1之外,其餘細胞的狀態值都是0的基礎CA。
以Wolfram標準做法中所使用的基礎CA作為初代CA,並使用前面圖中的規則集來迭代,然後將各世代的CA排列在一起,可以得到下面的結果:
如果改用黑色方塊來表示狀態1,白色方塊表示狀態0,則畫出來的圖會長這樣:
這個圖案是碎形(fractal)中著名的Sierpiński三角形(Sierpiński triangle)。把方塊縮小成像素大小讓解析度高一些,可以看得更清楚:
在制定基礎CA的迭代規則集時,我們先列出3個鄰域細胞狀態值的所有可能排列組合,然後一一指定對應的新狀態值;這整個過程和結果充滿了0和1,實在是過於冗長、繁雜。那有沒有什麼辦法能讓它比較簡潔一點呢?利用Wolfram所使用的技巧就可以了。接下來就來看看Wolfram是怎麼做的。
在規則集中,3個鄰域細胞狀態值的排列組合共有8種,而這8種排列組合是永遠不會變的。所以,在規則集中,會變動的,就只有指定給這些排列組合的新狀態值。既然如此,那就規定3個鄰域細胞狀態值的排列組合順序為
111 110 101 100 011 010 001 000
這樣子當指定的新狀態值時,不需多說,就會知道哪個排列組合是對應到哪個新狀態值了。例如,如果新狀態值是
0 1 0 1 1 0 1 0
那就表示對應的方式是
111 ⟶ 0
110 ⟶ 1
101 ⟶ 0
100 ⟶ 1
011 ⟶ 1
010 ⟶ 0
001 ⟶ 1
000 ⟶ 0
如果把上面那個對應的新狀態值看成是8位數的2進位整數,轉成10進位之後,可以得到整數90,也就是
010110102 = 90
Wolfram就把這組規則集叫做「規則90」(rule 90)。
既然規則集中的新狀態值是個8位數的2進位整數,那總共就會有256個不同的規則集。當看到「規則N」時,只要把N轉成2進位整數,就可以知道真正的規則集是長啥樣子了。例如,以規則222來說,因為
222 = 110111102
所以對應方式就是
111 ⟶ 1
110 ⟶ 1
101 ⟶ 0
100 ⟶ 1
011 ⟶ 1
010 ⟶ 1
001 ⟶ 1
000 ⟶ 0
而迭代畫出來的圖就長這樣:
規則集總共有256個,雖然不是每一個都可以畫出像Sierpiński三角形那樣讓人驚嘆的圖案,但的確有些圖案跟自然界中常見的圖案極其類似。例如,規則30所畫出來的圖案長這樣:
這個圖案就跟一種蝸牛外殼的花紋非常相像。從這個例子就可以看出來,CA在模擬和模式生成等領域中,是具有多麼大的價值了。
寫細胞自動機程式時,可以用物件導向的方式來寫,也可以就用List
這個資料結構來寫,畢竟CA就只是一堆0和1的組合而已。現在就先用List
來寫,物件導向式的寫法,在後面會有一節專門來介紹。
要怎麼用List
這個資料結構來表示1D CA呢?這其實挺簡單的,就這樣寫:
cells = [1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0]
這樣cells
的每個元素,就對應網格中的一個細胞,而放在元素中的數字,就是細胞的狀態值。所以,cells[0]
就是最左邊的那個細胞的狀態,而cells[-1]
就是最右邊的那個細胞的狀態。
有了細胞的狀態值之後,就可以利用規則集來計算CA在下一個世代時的狀態值了。不過,在開始寫程式之前,要先解決一個問題:計算下一個世代的狀態值時,需要用到鄰域的3個細胞狀態值,但是位於最左和最右兩邊的細胞,它們的鄰域只有兩個細胞,那該怎麼辦?這個問題有三種處理法式:
cells[0]
,它的鄰域會是cells[-1]
、cells[0]
、cells[1]
;而最尾端那個細胞,也就是cells[-1]
,它的鄰域會是cells[-2]
、cells[-1]
、cells[0]
。這應該是最廣為採用的處理方式了。在這裡,我們會採用第一種方式來處理首、尾細胞的狀態問題;這樣程式比較好寫,也比較容易閱讀。根據這樣子的設定,要計算CA在下一個世代時的狀態值,程式可以這樣寫:
next_generation = cells.copy()
for i in range(1, len(cells)-1):
left = cells[i-1]
middle = cells[i]
right = cells[i+1]
next_generation[i] = rules(left, middle, right)
cells = next_generation.copy()
這裡要注意的是,for
迴圈是掐頭去尾從1
到len(cells)-1
,並未包含首、尾兩個細胞,因為它們的狀態值跟上一代是一樣的,並不會改變,所以不用處理。另外,在最後一行,我們把CA在下一個世代時的狀態值複製到cells
中,藉此將cells
更新為最新一代的細胞,以便繼續進行下一次的迭代。除了這兩點之外,在程式裡頭,還有個用來計算新狀態值的函數rules()
尚待完成。
那個尚待完成的函數rules()
要怎麼寫呢?這就要從規則集的設定方式談起了。我們在設定規則集時,會列出鄰域細胞狀態值的所有可能排列組合,也就是下列這8種排列組合:
111 110 101 100 011 010 001 000
然後分別指定對應的新狀態值給每一種排列組合。如果把這些排列組合看成是3位數的2進位整數,那這些整數會是
7 6 5 4 3 2 1 0
假設我們把規則集放在ruleset
這個List
中,那ruleset[0]
裡頭放的,就是排列組合7的新狀態值;ruleset[1]
裡頭放的,則是排列組合6的新狀態值,以此類推。所以,rules()
這個函數,就可以這樣寫:
def rules(left, middle, right, ruleset):
idx = 4*left + 2*middle + right
return ruleset[7-idx]
之所以要把用來計算新狀態值的部分寫成函數,是考慮到如果新狀態值的計算方式有所不同時,只需修改rules()
這個函數就可以,而不需動到程式的主架構。
接下來,要來看看關於規則集寫法的部分,這部分原書並未討論。
規則集有兩種表示方式,一種是直接寫出細胞狀態值,例如「01011010」;另一種則是使用10進位整數,例如「規則90」。這兩種寫法互相轉換的程式可以這樣寫:
# 從狀態值轉成整數
ruleset = [0, 1, 0, 1, 1, 0, 1, 0]
rule_string = ''.join(str(c) for c in ruleset)
rule_number = int(rule_string, 2)
# 從整數轉成狀態值
rule_number = 90
ruleset = [int(c) for c in f'{rule_number:08b}']
從整數轉成狀態值時需要先將整數轉成二進位字串,bin()
函數也可以做到這一點,不過會在字串前面加上0b
兩個字母,這樣會增加後續處理上的麻煩。所以,直接使用f-string來轉換,會是比較省時省力的做法。
要畫基礎CA的圖案,標準做法是把一代一代的CA堆疊在一起,並分別用黑色和白色方塊來呈現狀態值為1和0的細胞。下面這張圖,就是由規則90的CA所製作出來的圖案:
這裡要注意的是,雖然這是張2D的圖片,但這並不代表它是個2D CA;它只是由1D CA的不同世代所堆疊而成,方便呈現CA迭代過程的一張圖片而已。所以,CA的圖案的畫法,並非是絕對的,完全可以依照實際的需要而加以改變。
下面這個例子所呈現的,是使用標準做法所畫出的規則90基礎CA圖案,不過為了減少一點計算量,就只畫出狀態值是1的細胞而已。
Example 7.1: Wolfram Elementary Cellular Automata
def rules(left, middle, right, ruleset):
idx = 4*left + 2*middle + right
return ruleset[7-idx]
# python version 3.10.9
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Example 7.1: Wolfram Elementary Cellular Automata")
WHITE = (255, 255, 255)
screen_size = WIDTH, HEIGHT = 640, 240
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
# 細胞方塊邊長
cell_size = 10
# 細胞數量
n_cells = WIDTH // cell_size
# 世代數量
n_generation = HEIGHT // cell_size
# 規則90
rule_number = 90
ruleset = [int(c) for c in f'{rule_number:08b}']
# 初代CA
cells = [0]*n_cells
cells[n_cells//2] = 1
next_generation = cells.copy()
screen.fill(WHITE)
for generation in range(n_generation):
for i in range(n_cells):
# 只畫出狀態值為1的細胞
if cells[i] == 1:
rect = pygame.Rect(i*cell_size, generation*cell_size, cell_size, cell_size)
pygame.draw.rect(screen, (0, 0, 0), rect)
for i in range(1, n_cells-1):
left = cells[i-1]
middle = cells[i]
right = cells[i+1]
next_generation[i] = rules(left, middle, right, ruleset)
cells = next_generation.copy()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
pygame.display.update()
frame_rate.tick(FPS)
Exercise 7.1
為了避免畫面更換速度太快而看不清楚,所以把FPS
設定為1
。
def rules(left, middle, right, ruleset):
idx = 4*left + 2*middle + right
return ruleset[7-idx]
# python version 3.10.9
import random
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 7.1")
WHITE = (255, 255, 255)
screen_size = WIDTH, HEIGHT = 640, 240
screen = pygame.display.set_mode(screen_size)
FPS = 1
frame_rate = pygame.time.Clock()
# 細胞方塊邊長
cell_size = 10
# 細胞數量
n_cells = WIDTH // cell_size
# 世代數量
n_generation = HEIGHT // cell_size
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
# 隨機選取的規則集
ruleset = [random.randint(0, 1) for _ in range(8)]
# 初代CA
cells = [0]*n_cells
cells[n_cells//2] = 1
next_generation = cells.copy()
for generation in range(n_generation):
for i in range(n_cells):
if cells[i] == 1:
rect = pygame.Rect(i*cell_size, generation*cell_size, cell_size, cell_size)
pygame.draw.rect(screen, (0, 0, 0), rect)
for i in range(1, n_cells-1):
left = cells[i-1]
middle = cells[i]
right = cells[i+1]
next_generation[i] = rules(left, middle, right, ruleset)
cells = next_generation.copy()
pygame.display.update()
frame_rate.tick(FPS)
Exercise 7.2
使用規則90,以下為其中一張圖片
def rules(left, middle, right, ruleset):
idx = 4*left + 2*middle + right
return ruleset[7-idx]
# python version 3.10.9
import random
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 7.2")
WHITE = (255, 255, 255)
screen_size = WIDTH, HEIGHT = 640, 240
screen = pygame.display.set_mode(screen_size)
FPS = 1
frame_rate = pygame.time.Clock()
# 細胞方塊邊長
cell_size = 10
# 細胞數量
n_cells = WIDTH // cell_size
# 世代數量
n_generation = HEIGHT // cell_size
rule_number = 199
ruleset = [int(c) for c in f'{rule_number:08b}']
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
# 初代CA的細胞狀態是隨機的
cells = [random.randint(0, 1) for _ in range(n_cells)]
next_generation = cells.copy()
for generation in range(n_generation):
for i in range(n_cells):
if cells[i] == 1:
rect = pygame.Rect(i*cell_size, generation*cell_size, cell_size, cell_size)
pygame.draw.rect(screen, (0, 0, 0), rect)
for i in range(1, n_cells-1):
left = cells[i-1]
middle = cells[i]
right = cells[i+1]
next_generation[i] = rules(left, middle, right, ruleset)
cells = next_generation.copy()
pygame.display.update()
frame_rate.tick(FPS)
Exercise 7.3
細胞圍繞畫面中心排成圓形,不同世代的細胞顏色不同。
def rules(left, middle, right, ruleset):
idx = 4*left + 2*middle + right
return ruleset[7-idx]
# python version 3.10.9
import random
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 7.3")
WHITE = (255, 255, 255)
screen_size = WIDTH, HEIGHT = 640, 360
screen = pygame.display.set_mode(screen_size)
screen_center = pygame.Vector2(WIDTH//2, HEIGHT//2)
FPS = 60
frame_rate = pygame.time.Clock()
# 細胞方塊邊長
cell_size = 2
# 細胞數量
n_cells = WIDTH // cell_size
# 世代數量
n_generation = HEIGHT // cell_size
# 規則90
rule_number = 90
ruleset = [int(c) for c in f'{rule_number:08b}']
# 初代CA
cells = [0]*n_cells
cells[n_cells//2] = 1
screen.fill(WHITE)
next_generation = cells.copy()
for generation in range(n_generation):
for i in range(n_cells):
if cells[i] == 1:
# 細胞所在位置
r, theta = generation*cell_size/2.3, i*cell_size
position = screen_center + pygame.Vector2.from_polar((r, theta))
# 細胞顏色
r, g, b = 7*generation%255, 17*generation%255, 37*generation%255
color = (r, g, b)
pygame.draw.circle(screen, color, position, cell_size/2)
for i in range(1, n_cells-1):
left = cells[i-1]
middle = cells[i]
right = cells[i+1]
next_generation[i] = rules(left, middle, right, ruleset)
cells = next_generation.copy()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
pygame.display.update()
frame_rate.tick(FPS)
Exercise 7.4
儲存每幀畫面的第1代CA,並將其作為下一幀畫面的初代CA即可。
def rules(left, middle, right, ruleset):
idx = 4*left + 2*middle + right
return ruleset[7-idx]
# python version 3.10.9
import random
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 7.4")
WHITE = (255, 255, 255)
screen_size = WIDTH, HEIGHT = 640, 240
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
# 細胞方塊邊長
cell_size = 1
# 細胞數量
n_cells = WIDTH // cell_size
# 世代數量
n_generation = HEIGHT // cell_size
rule_number = 90
ruleset = [int(c) for c in f'{rule_number:08b}']
# 初代CA
cells_start = [0]*n_cells
cells_start[n_cells//2] = 1
cells = cells_start.copy()
next_generation = cells.copy()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
cells = cells_start.copy()
for generation in range(n_generation):
# 儲存第1代CA,作為下一幀畫面中的初代CA
if generation == 1:
cells_start = cells.copy()
for i in range(n_cells):
if cells[i] == 1:
rect = pygame.Rect(i*cell_size, generation*cell_size, cell_size, cell_size)
pygame.draw.rect(screen, (0, 0, 0), rect)
for i in range(1, n_cells-1):
left = cells[i-1]
middle = cells[i]
right = cells[i+1]
next_generation[i] = rules(left, middle, right, ruleset)
cells = next_generation.copy()
pygame.display.update()
frame_rate.tick(FPS)