用MATLAB复现Vicsek模型:自驱动粒子群集运动的仿真与相变分析
2026/9/15 13:59:21 网站建设 项目流程

简介:基于Matlab的Vicsek模型演示程序,可用于模拟鸟类、鱼类等群体中的自组织运动与集体行为,适合计算机、电子信息工程、数学等专业学生用于课程设计、期末大作业和毕业设计,也可作为复杂系统与多智能体研究的入门工具。压缩包共10个文件,整体仅1.64MB,主要包含5个m脚本、1个mlapp交互式应用、1个exe安装程序以及截图、说明文档和许可证文件,脚本覆盖方向计算、箭头绘制、流场构建、曼哈顿距离计算与主控运行等模块,结构清晰,便于按需调用。已有79人学习下载。代码采用参数化编程,关键参数可方便调整,注释细致,并附带可直接运行的案例数据,支持Matlab2014/2019a/2024a多个版本。借助mlapp图形界面与独立exe安装包,即使不熟悉Matlab的读者也能快速观察不同个体速度、密度条件下群体运动模式的变化,为理解个体规则如何涌现出整体有序行为提供直观、可二次开发的仿真平台。

1. 用 MATLAB 复现 Vicsek 模型:自组织运动的入场方式

一群鸟在天上集体转向,并不靠领袖指挥。1995 年 Vicsek 等人提出的自驱动粒子模型,把“模仿邻居平均方向”这条局部规则数学化,成为解释集群运动与自组织相变的基线模型。用 MATLAB 复现它的价值很直接:矩阵化计算一次更新全部粒子,quiver 等图形函数让演化过程肉眼可见,既适合课堂演示,也适合作为后续群集研究的起点。

这条模型只有两条规则:粒子以恒定速率移动,同时不断把方向调整为邻居平均方向并附加随机扰动。任何 R2019b 以上的 MATLAB 环境都无需额外工具箱即可跑通。核心要素有四个:邻居范围 R、噪声强度 η、速率 v、密度 N/L²。下面从数学定义开始,逐层落到可运行代码与参数分析。

2. 从公式到 MATLAB 变量:Vicsek 模型的核心定义与初始化

2.1 一条规则:速度恒定,方向朝邻居平均

在二维平面上,第 i 个粒子的状态由三元组 (x_i, y_i, θ_i) 描述,其中 θ_i 是运动方向角。模型每个时间步做两步更新。

方向更新的原始表达式是:θ_i(t+1) = ⟨θ_j(t)⟩_|r_i-r_j|<R + Δθ。这里的 ⟨·⟩ 表示对邻居集合内的方向求平均,Δθ 是均匀分布在 [-η/2, η/2] 上的随机扰动。关键点是,这个平均操作不能直接对角度做算术平均,而要先把每个角度换成单位向量,叠加后再用 atan2 恢复角度。原因很简单:设两个粒子方向分别为 170° 和 -170°,算术平均值是 0°,但真实“中间方向”应该是 180°。用单位向量叠加天然避开 ±π 跳变,这是实现时最容易踩的坑。

位置更新则比较直观:新方向确定后,粒子沿该方向以速率 v 前进一个时间步:

x_i(t+1) = x_i(t) + v·cosθ_i(t+1) y_i(t+1) = y_i(t) + v·sinθ_i(t+1)

注意位置更新使用的是 t+1 时刻的新方向,而不是 t 时刻的旧方向。方向更新与位置更新的先后顺序不能换,否则模型就退化成了“先移动再转向”的另类随机游走,宏观有序性会显著降低。

2.2 周期边界下的邻居判定:最小镜像约定

模拟区域是边长 L 的正方形,研究者普遍使用周期边界条件,粒子从右边界离开就出现在左边界。计算粒子间距离时,必须考虑“最小镜像”:两粒子在周期空间里可能有多个副本,实际距离取所有副本中最近的那个。实现时只需要一步修正:

dx = pos(:,1) - pos(i,1); dy = pos(:,2) - pos(i,2); dx = dx - round(dx / L) * L; % 最小镜像:把差值折叠到 [-L/2, L/2] dy = dy - round(dy / L) * L; dist2 = dx.^2 + dy.^2; % 距离平方,避免开方开销

这段代码中,pos 是 N×3 的状态矩阵,pos(:,1) 取出所有粒子的 x 坐标。dx、dy 是 N×1 列向量,round(dx / L) * L 把越界量按周期折叠回去。这里用距离平方与 R^2 比较,省去了 sqrt 计算;在 N 较大时,少一次全向量开方能明显缩短耗时。

周期边界如果处理错,最直接的现象是边界附近出现一条“虚拟缝隙”:原本跨边界的邻居被算成远处的点,导致集群在边界处断裂或形成带状伪影。固定参数、改变边界处理方式后对比动画,可以快速确认自己是否写对了。

2.3 初始化参数:随机撒点与可复现实验

模拟开始前,把 N 个粒子随机撒在 [0, L]×[0, L] 区域内,方向角均匀分布在 [-π, π]。为了结果可复现,用 rng 固定随机种子。典型配置如下:

% 基本参数 N = 300; % 粒子总数 L = 10; % 方形区域边长 eta = 0.5; % 噪声强度(弧度) v = 0.1; % 粒子运动速率 R = 1.0; % 邻居半径 T = 800; % 迭代总步数 rng(42); % 固定种子,复现实验 pos = rand(N, 2) * L; % 位置均匀分布在 [0, L]^2 theta = rand(N, 1) * 2 * pi - pi; % 方向角均匀分布在 [-pi, pi]

rand(N,2) 生成 N 行 2 列 [0,1] 均匀随机数,乘 L 后映射到区域空间。rand(N,1)2pi-pi 把方向映射到 [-π, π]。需要说明的是,粒子方向取均匀分布意味着系统初始态完全无序,后面观察到的有序必须完全由相互作用自发产生。

常用参数速查表如下:

参数符号示例值对行为的影响
粒子数N300密度过低时难以建立长程有序
区域边长L10与 N 共同决定密度 ρ = N/L²
噪声强度η0.5增大则系统走向无序,存在相变点
粒子速率v0.1影响粒子单步迁移距离
邻居半径R1.0信息交流范围,越大同步越容易
迭代步数T800必须覆盖暂态才能统计稳态值

表中的 η 是最值得关注的参数。固定其余参数、扫描 η 时,系统从高度有序过渡到无序并不是线性的,而是在某个临界值附近发生快速转折,这就是 Vicsek 模型著名的连续相变现象。

3. 用 MATLAB 写全量代码:Vicsek 模型的主循环与动画

3.1 主循环:邻居搜索、方向平均与位置推进

初始化完成后的核心迭代结构是双层循环:外层遍历时间步,内层遍历粒子。每个粒子的邻居集合用一次向量化距离计算得到,不写成双重粒子循环,这充分利用了 MATLAB 的矩阵运算特性。

% vicsek_main.m(接续初始化代码) phi_record = zeros(T, 1); % 记录每时刻序参量 for t = 1:T new_theta = zeros(N, 1); % 暂存新方向,避免污染本时刻数据 for i = 1:N % 粒子 i 的坐标 xi = pos(i,1); yi = pos(i,2); % 与所有粒子的最小镜像距离 dx = pos(:,1) - xi; dy = pos(:,2) - yi; dx = dx - round(dx / L) * L; dy = dy - round(dy / L) * L; % 邻居掩码:以 R 为半径,包含自身 is_neighbor = (dx.^2 + dy.^2) < R^2; % 单位向量叠加求平均方向 sum_sin = sum(sin(theta(is_neighbor))); sum_cos = sum(cos(theta(is_neighbor))); avg_theta = atan2(sum_sin, sum_cos); % 添加均匀随机噪声,范围 [-eta/2, eta/2] new_theta(i) = avg_theta + (rand - 0.5) * eta; end % 方向全部更新完,再统一更新位置 theta = new_theta; pos(:,1) = pos(:,1) + v * cos(theta); pos(:,2) = pos(:,2) + v * sin(theta); % 周期边界折叠 pos(:,1) = mod(pos(:,1), L); pos(:,2) = mod(pos(:,2), L); % 计算序参量 phi(3.3 详述) phi = abs( sum( exp(1i * theta) ) ) / N; phi_record(t) = phi; end

这段代码有三个设计细节必须说明。

第一,new_theta 暂存数组是必须的。如果循环内直接写 theta(i),紧随其后的粒子 j 在做邻居平均时就会读到 i 的新方向,造成同一时间步内更新顺序敏感的错误。实际表现虽然看起来也是“同步”,但数值上已经不是原始模型。

第二,is_neighbor 掩码把粒子自身也包含在邻居集合里。这是 Vicsek 原始模型的约定:自己的方向参与平均,结果等于在邻居平均里多了一个自加强项。若要把自己排除,必须额外写一行is_neighbor(i) = false;,两种写法都有论文使用,但物理含义略有差别。

第三,(rand - 0.5) * eta中 rand 产生 [0,1] 均匀分布,减 0.5 后落在 [-0.5, 0.5],乘 eta 即得 [-η/2, η/2]。如果在其他代码里看到用 randn 生成高斯噪声,那是另一种噪声模型,不要与 Vicsek 原始定义混用。

3.2 用 quiver 画图并导出 GIF:让模型动起来

运行主循环后,最容易看到现象的方式是画 quiver 箭头图。每帧给每个粒子画一个箭头,方向与 θ 对齐。需要调整箭头缩放因子,因为速度 v 本身很小,开自动缩放时箭头会短到几乎不可见。常见做法是把绘制向量放大到区域尺寸的 5% 左右,并把 quiver 第五个参数设为 0 以关闭自动缩放:

% 动画显示 figure('Color', 'w', 'Position', [100 100 560 480]); arrow_scale = L * 0.05; % 让箭头长度约为边长的 5% for t = 1:T clf; quiver(pos(:,1), pos(:,2), ... arrow_scale * cos(theta), arrow_scale * sin(theta), 0, ... 'LineWidth', 1.2, 'Color', [0 0.45 0.74]); xlim([0 L]); ylim([0 L]); axis equal tight; title(sprintf('Vicsek model: t = %d, \\phi = %.3f', t, phi_record(t))); grid on; drawnow; % 每隔 5 帧追加写入 GIF frame = getframe(gcf); [A, map] = rgb2ind(frame.cdata, 256); if t == 1 imwrite(A, map, 'vicsek.gif', 'gif', ... 'LoopCount', Inf, 'DelayTime', 0.05); elseif mod(t, 5) == 0 imwrite(A, map, 'vicsek.gif', 'gif', ... 'WriteMode', 'append', 'DelayTime', 0.05); end end

quiver 的第五个参数 0 表示不自动缩放,箭头长度直接用 u、v 向量决定。arrow_scale 取 L×0.05 是为了视觉清晰,不影响数据本身的数值。drawnow 强制刷新图形窗口,缺少它 MATLAB 会积攒帧到脚本结束才一次性绘制,动画感尽失。GIF 写入时第一帧要单独用 imwrite 创建并指定 LoopCount,后续帧通过 WriteMode 为 append 反复追加;DelayTime 0.05 秒对应每秒 20 帧,较流畅。

动画参数速查:

绘制参数作用建议值
quiver 第 5 参0 表示关闭自动缩放0
arrow_scale箭头显示长度L×0.03~0.08
DelayTimeGIF 帧间隔0.03~0.1
mod(t,5) 抽样降低 GIF 体积帧率过高时改为 10

若脚本单步刷新太慢,把绘图代码放入if mod(t,5)==0条件块,每 5 步只画一次。动画流畅度和计算速度之间的平衡点,通常画 50~200 帧就足够展示相变过程。

3.3 序参量与稳态平均值:把有序度量化输出

视觉判断终究是主观的。Vicsek 模型最常用的定量输出是序参量 φ:

φ = (1/N) · | Σ_{j=1}^{N} exp(iθ_j) |

所有粒子方向一致时 φ=1,完全随机时 φ 接近 0。代码中一行即可计算:

phi = abs(sum(exp(1i * theta))) / N;

exp(1i*theta) 把每个方向角映射到复平面单位圆上,sum 做矢量叠加,abs 取模长后再除以 N。由于是在复平面上的和,方向夹角小的粒子会互相加强,夹角大的会彼此抵消,这一数学形式天然实现了“向量平均”的效果。

φ_record 记录的是瞬时值。系统从初始随机构型演化到致密状态需要一段弛豫时间,前几十步通常变化很快,因此计算稳态有序度时只取后一半的平均:

steady_mean = mean(phi_record(round(T/2):end));

取后一半而非全局平均,是因为初始随机配置产生的低 φ 值会低估系统真实有序度。参数 T 的设置也要保证后一半足够长,一般至少 200 步以上。

4. Vicsek 模型的参数灵敏度分析与 MATLAB 调试要点

4.1 噪声、密度与邻居半径如何决定有序和无序

在默认参数下扫描 η,运动状态大致可以划分成三个区域:

η 范围稳态 φ 区间宏观表现
η < 0.10.9~1.0所有粒子同向移动,集群稳定
0.1 ≤ η ≤ 2.00.2~0.9多集群并存,方向不断变化
η > 2.5< 0.2粒子方向近似随机,类气体态

低噪声下,局部对齐信息通过邻居关系不断传播,最终全局方向锁定一致。噪声增大后,每一步的随机扰动削弱了传播中的方向信息,系统分裂成多个临时小集群。噪声更大时,扰动完全压制对齐信号,每个粒子近似独立随机行走。

密度的影响可以从邻居数量角度来理解。固定 R 时,密度 ρ=N/L² 越高,每个粒子邻近的平均粒子数越多,平均方向包含的样本量就越大,系统抗噪能力越强。等效地,固定密度而增大 R,也能让单个粒子的信息源变多,进而提升有序度。换句话说,真正决定信息同步程度的核心变量不是 η 或 R 单独一个,而是“对齐信号强度”与“噪声扰动强度”的比值。

演示项目建议把 N 设为 300~500,R 设为粒子平均间距的 1~2 倍。实际代码中平均间距约等于 L/sqrt(N),对 N=300、L=10 而言约为 0.58,所以 R 取 1.0 刚好覆盖最近一两圈粒子,现象最典型。

4.2 两个不报错但结果全错的经典陷阱

4.2.1 角度回绕被忽略

如果实现时不使用单位向量叠加,而是先对邻居角度求算术平均,就会遇到 ±π 边界问题。例如 170° 与 -170° 的算术平均是 0°,正确的对齐方向应在 180° 附近。用一句话检验方法:初始化两个粒子,角度 170° 和 -170°,设置 η=0 且互为邻居。若你的代码算出的方向是 0°,说明平均逻辑是错的;若结果是 180° 附近,才符合向量平均语义。

4.2.2 排序破坏了行索引对应关系

is_neighbor 掩码和 theta 向量严格按行索引对应。如果中途对 pos 或 theta 做了 sortrows 或用 find 取子集后再写回,就会破坏对应关系。表面不报错,但邻居平均用的方向属于另一个粒子,物理上完全失真。我的建议是全程保持状态矩阵行序不变,若需要按某种规则排序,就额外维护一个索引向量 idx,通过 pos(idx,:) 间接访问。

此外还要意识到,当 R 小于粒子间距时部分粒子可能长时间没有邻居,方向更新只剩自身加噪声,近似随机游走。这在物理上合理,但如果你期望看到大范围有序,就需要把 R 调到能够覆盖到多数粒子。

4.3 性能优化:向量化、预分配与常量提取

3.1 的实现使用“外层循环粒子、内层向量化距离”的折中方案。对 N ≤ 2000 的场景,这个方案在 MATLAB 的 JIT 加速下表现不错,而且修改规则时直观。若想进一步提速,优先做三件事:

% 1. 预分配所有输出数组 theta_record = zeros(T, N); % 禁止用 [theta_record; theta] 动态拼接 % 2. 循环外提取常量 v_dt = v * dt; % 避免每个时间步都做乘法 % 3. 避免重复计算三角函数 cos_t = cos(theta); sin_t = sin(theta); % 位置更新改为 pos(:,1) = pos(:,1) + v_dt * cos_t;

动态拼接数组会导致 MATLAB 反复申请内存,T=800 时影响不大,但 T 上万后非常明显。把 v*dt 提出循环外至少省掉 T 次乘法。更关键的优化是把 cos(theta) 存入临时变量,因为 theta 未更新前每次调用都做全向量三角函数计算,代价高昂。

当 N 超过 2000 时,逐粒子循环的 O(N²) 复杂度会显著拖慢速度。此时应当引入rangesearch(Statistics and Machine Learning Toolbox)或 KD 树求邻居,把复杂度降到 O(NlogN)。如果追求极致性能,可以考虑用并行 for 循环,但内层计算必须无依赖共享变量,否则并行后结果不可复现。

5. 相变验证与 Vicsek 模型的扩展实验

5.1 重现噪声-序参量相变曲线

要从演示走向有说服力的结果,建议把主循环封装成函数,批量扫描 η 并绘制 φ-η 曲线。封装后的输入输出清晰,多组参数可复现调用。

function phi_mean = run_vicsek(N, L, eta, v, R, T) rng(42); % 每次用相同初始构型,保证可比性 pos = rand(N,2) * L; theta = rand(N,1) * 2*pi - pi; phi_record = zeros(T,1); for t = 1:T new_theta = zeros(N,1); for i = 1:N dx = pos(:,1) - pos(i,1); dy = pos(:,2) - pos(i,2); dx = dx - round(dx / L) * L; dy = dy - round(dy / L) * L; mask = (dx.^2 + dy.^2) < R^2; new_theta(i) = atan2(sum(sin(theta(mask))), ... sum(cos(theta(mask)))) + ... (rand - 0.5) * eta; end theta = new_theta; pos(:,1) = pos(:,1) + v * cos(theta); pos(:,2) = pos(:,2) + v * sin(theta); pos(:,1) = mod(pos(:,1), L); pos(:,2) = mod(pos(:,2), L); phi_record(t) = abs(sum(exp(1i*theta))) / N; end phi_mean = mean(phi_record(round(T/2):end)); end

扫描脚本:

etas = 0:0.2:3; phi_means = zeros(size(etas)); for k = 1:numel(etas) phi_means(k) = run_vicsek(300, 10, etas(k), 0.1, 1.0, 500); end plot(etas, phi_means, '-o', 'LineWidth', 1.5); xlabel('噪声强度 \eta'); ylabel('稳态序参量 \phi'); grid on;

注意 rng(42) 让每个噪声点都用相同初始构型,消除了不同初始条件带来的差异,代价是曲线可能带少量随机毛刺。想得到更平滑的曲线,可以把 rng(42) 换成 rng(k)(k 为扫描索引),或者对每个点运行多次取平均。

提示:如果 φ 在高噪声区间不趋近 0,先检查是否忘记加周期边界折叠,或者噪声范围写成了 [0, η] 而非 [-η/2, η/2]。

5.2 扩展:从纯对齐到带惯性的 Vicsek 模型

基础代码熟悉后,最简单的有效扩展是给方向更新加入惯性项:粒子不仅要向邻居对齐,还要部分保留自身旧方向。把邻居平均和自身方向按权重叠加:

alpha = 0.7; % 邻居平均权重,越小惯性越强 for i = 1:N % mask 计算同 3.1 节主循环 own = [cos(theta(i)), sin(theta(i))]; avg = [sum(cos(theta(mask))), sum(sin(theta(mask)))]; blended = alpha * avg + (1 - alpha) * own; % 加权合成 new_theta(i) = atan2(blended(2), blended(1)) + (rand - 0.5) * eta; end

alpha=1 时退化为原始 Vicsek 模型,alpha 越小粒子越“执拗”,系统收敛到全局有序所需的时间随之延长,暂态过程会表现出更丰富的涡旋结构。把 alpha 从 1 逐步降到 0.5,并记录稳态 φ 与弛豫时间,可以定量理解惯性对自组织速度的抑制。这种带惯性项的变体在行人动力学和机器人编队里都有对应版本,是成本最低但有实际研究意义的扩展方向。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询