元胞自动机 是离散模型,通常在网格上,随着时间演化。每个网格单元都有一个有限的状态,例如 0 或 1,它根据一组特定的规则进行更新。一个特定的单元使用周围单元的信息(称为它的邻域)来确定应该进行哪些更改。一般来说,元胞自动机可以在任意数量的维度中定义。一个著名的二维例子是康威生命游戏,其中细胞“存活”和“死亡”,有时会产生美丽的图案。
在这篇文章中,我们将研究一个称为初等元胞自动机的一维示例,该示例由史蒂芬·沃尔夫勒姆在 1980 年代推广。
想象一排并排排列的细胞,每个细胞都是黑色或白色。我们将黑色细胞标记为 1,白色细胞标记为 0,从而形成一个比特数组。例如,让我们考虑一个随机的 20 比特数组。
import numpy as np
rng = np.random.RandomState(42)
data = rng.randint(0, 2, 20)
print(data)
[0 1 0 0 0 1 0 0 0 1 0 0 0 0 1 0 1 1 1 0]
为了更新我们的元胞自动机的状态,我们需要定义一组规则。给定的单元格\(C\)只知道其左右邻居的状态,分别标记为\(L\)和\(R\)。我们可以定义一个函数或规则\(f(L, C, R)\),它将单元格状态映射到 0 或 1。
由于我们的输入单元格是二进制值,因此函数有\(2^3=8\)种可能的输入。
for i in range(8):
print(np.binary_repr(i, 3))
000
001
010
011
100
101
110
111
对于每个输入三元组,我们可以为输出分配 0 或 1。\(f\)的输出是将在下一个时间步长中替换当前单元格\(C\)的值。总共有\(2^{2^3} = 2^8 = 256\)种可能的规则来更新单元格。史蒂芬·沃尔夫勒姆引入了一种命名约定,现在称为沃尔夫勒姆代码,用于更新规则,其中每个规则都由一个 8 位二进制数字表示。
例如,“规则 30”可以通过首先转换为二进制,然后为每个比特构建一个数组来构建
rule_number = 30
rule_string = np.binary_repr(rule_number, 8)
rule = np.array([int(bit) for bit in rule_string])
print(rule)
[0 0 0 1 1 1 1 0]
按照惯例,沃尔夫勒姆代码将前导比特与“111”关联,将最后一位与“000”关联。对于规则 30,输入、规则索引和输出之间的关系如下
for i in range(8):
triplet = np.binary_repr(i, 3)
print(f"input:{triplet}, index:{7-i}, output {rule[7-i]}")
input:000, index:7, output 0
input:001, index:6, output 1
input:010, index:5, output 1
input:011, index:4, output 1
input:100, index:3, output 1
input:101, index:2, output 0
input:110, index:1, output 0
input:111, index:0, output 0
我们可以定义一个函数,将输入单元格信息与关联的规则索引映射。从本质上讲,我们将二进制输入转换为十进制并调整索引范围。
def rule_index(triplet):
L, C, R = triplet
index = 7 - (4 * L + 2 * C + R)
return int(index)
现在,我们可以获取任何输入并根据我们的规则查找输出,例如
rule[rule_index((1, 0, 1))]
0
最后,我们可以使用 Numpy 创建一个包含我们状态数组所有三元组的数据结构,并在适当的轴上应用该函数以确定我们的新状态。
all_triplets = np.stack([np.roll(data, 1), data, np.roll(data, -1)])
new_data = rule[np.apply_along_axis(rule_index, 0, all_triplets)]
print(new_data)
[1 1 1 0 1 1 1 0 1 1 1 0 0 1 1 0 1 0 0 1]
这就是我们元胞自动机单次更新的过程。
为了进行多次更新并在一段时间内记录状态,我们将创建一个函数。
def CA_run(initial_state, n_steps, rule_number):
rule_string = np.binary_repr(rule_number, 8)
rule = np.array([int(bit) for bit in rule_string])
m_cells = len(initial_state)
CA_run = np.zeros((n_steps, m_cells))
CA_run[0, :] = initial_state
for step in range(1, n_steps):
all_triplets = np.stack(
[
np.roll(CA_run[step - 1, :], 1),
CA_run[step - 1, :],
np.roll(CA_run[step - 1, :], -1),
]
)
CA_run[step, :] = rule[np.apply_along_axis(rule_index, 0, all_triplets)]
return CA_run
initial = np.array([0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0])
data = CA_run(initial, 10, 30)
print(data)
[[0. 1. 0. 0. 0. 1. 0. 0. 0. 1. 0. 0. 0. 0. 1. 0. 1. 1. 1. 0.]
[1. 1. 1. 0. 1. 1. 1. 0. 1. 1. 1. 0. 0. 1. 1. 0. 1. 0. 0. 1.]
[0. 0. 0. 0. 1. 0. 0. 0. 1. 0. 0. 1. 1. 1. 0. 0. 1. 1. 1. 1.]
[1. 0. 0. 1. 1. 1. 0. 1. 1. 1. 1. 1. 0. 0. 1. 1. 1. 0. 0. 0.]
[1. 1. 1. 1. 0. 0. 0. 1. 0. 0. 0. 0. 1. 1. 1. 0. 0. 1. 0. 1.]
[0. 0. 0. 0. 1. 0. 1. 1. 1. 0. 0. 1. 1. 0. 0. 1. 1. 1. 0. 1.]
[1. 0. 0. 1. 1. 0. 1. 0. 0. 1. 1. 1. 0. 1. 1. 1. 0. 0. 0. 1.]
[0. 1. 1. 1. 0. 0. 1. 1. 1. 1. 0. 0. 0. 1. 0. 0. 1. 0. 1. 1.]
[0. 1. 0. 0. 1. 1. 1. 0. 0. 0. 1. 0. 1. 1. 1. 1. 1. 0. 1. 0.]
[1. 1. 1. 1. 1. 0. 0. 1. 0. 1. 1. 0. 1. 0. 0. 0. 0. 0. 1. 1.]]
让我们开始可视化#
对于更大的模拟,有趣的模式开始出现。为了可视化我们的模拟结果,我们将使用ax.matshow
函数。
import matplotlib.pyplot as plt
plt.rcParams["image.cmap"] = "binary"
rng = np.random.RandomState(0)
data = CA_run(rng.randint(0, 2, 300), 150, 30)
fig, ax = plt.subplots(figsize=(16, 9))
ax.matshow(data)
ax.axis(False)
学习规则#
在设置好用于生成模拟的代码后,我们现在可以开始探索这些不同规则的属性。沃尔夫勒姆将规则分为四类,如下所述。
def plot_CA_class(rule_list, class_label):
rng = np.random.RandomState(seed=0)
fig, axs = plt.subplots(
1, len(rule_list), figsize=(10, 3.5), constrained_layout=True
)
initial = rng.randint(0, 2, 100)
for i, ax in enumerate(axs.ravel()):
data = CA_run(initial, 100, rule_list[i])
ax.set_title(f"Rule {rule_list[i]}")
ax.matshow(data)
ax.axis(False)
fig.suptitle(class_label, fontsize=16)
return fig, ax
第一类#
快速收敛到均匀状态的元胞自动机
_ = plot_CA_class([4, 32, 172], "Class One")
第二类#
快速收敛到重复或稳定状态的元胞自动机
_ = plot_CA_class([50, 108, 173], "Class Two")
第三类#
似乎保持随机状态的元胞自动机
_ = plot_CA_class([60, 106, 150], "Class Three")
第四类#
形成重复或稳定状态区域,但也形成以复杂方式相互作用的结构的元胞自动机。
_ = plot_CA_class([54, 62, 110], "Class Four")
令人惊奇的是,从规则 110 中出现的相互作用结构已被证明能够进行通用计算。
在以上所有示例中,都使用了随机初始状态,但另一个有趣的案例是当单个 1 初始化且所有其他值都设置为零时。
initial = np.zeros(300)
initial[300 // 2] = 1
data = CA_run(initial, 150, 30)
fig, ax = plt.subplots(figsize=(10, 5))
ax.matshow(data)
ax.axis(False)
对于某些规则,出现的结构以混乱且有趣的方式相互作用。
我希望您喜欢这次对初等元胞自动机世界的简要了解,并受到启发,自己创作一些漂亮的图片。