简介:这份资源面向参加数学建模竞赛(美赛)的学生及应急管理、公共安全领域的学习者,提供人员疏散过程建模仿真的参考代码,帮助理解人群在紧急情况下如何移动与撤离。压缩包共7个文件,以Matlab源程序(.m)为主,辅以说明文本、图形界面文件与备份文件,整体约29KB,体积轻便,便于快速阅读与二次修改。内容围绕社会力模型、元胞自动机模型、网络流模型等常见疏散建模思路展开,涉及个体受力、路径优化、场地布局与人群属性等输入设定,并可通过仿真结果分析疏散时间与拥挤程度。已有181人学习下载,适合作为赛题方案参考、算法复现与排错思路的入门素材,也可用于课程设计或应急疏散策略的初步验证。
1. 美赛人员疏散仿真代码包:从“跑通”到“跑出可信结论”的分水岭
每年美赛(MCM/ICM)E题、D题里,人员疏散几乎是绕不开的场景:体育馆看台、商场中庭、地铁站台、校园教学楼,题目给一张平面图、一组人流参数,要求你评估疏散时间、找瓶颈、给优化方案。很多人第一反应是打开搜索引擎找“美赛常见参考代码”,下载一个“人员疏散过程建模仿真代码.zip”,解压、改几个数字、跑出一张热力图,然后写进论文。问题是,评委一眼就能看出你的仿真是不是“真跑过”——因为疏散模型里最关键的几个参数(出口宽度、人员密度、预动作时间)只要有一个没交代清楚,结论就会自相矛盾。这个代码包真正值钱的地方,不是它替你算出了答案,而是它把“社会力模型 + 元胞自动机 + 离散事件调度”这三条主流路线的最小可运行骨架摆在你面前,让你能在48小时内改出一个逻辑自洽、参数可追溯、图表能复现的仿真。适合谁?适合已经会Python或MATLAB基础语法、但没系统做过行人流仿真、又不想从零推导微分方程的参赛者。下面我按“先选模型、再搭场景、后调参排错”的顺序,把这份代码包拆成能直接抄作业的步骤。
2. 先选对模型:社会力、元胞自动机、离散事件到底用哪个
2.1 三种路线的适用边界与计算代价
拿到“人员疏散过程建模仿真代码.zip”之后,别急着改main函数。先看它用的是哪套模型,因为不同模型对同一道题的结论可能差出30%以上。社会力模型(Social Force Model)把每个人看作受驱动力、人与人排斥力、人与墙排斥力共同作用的粒子,输出的是连续轨迹,适合分析“为什么出口前会形成拱形拥堵”“为什么双向人流会分层”。它的代价是计算量大:500人跑10分钟仿真,Python纯循环可能要几分钟,参数没调好还会出现“人穿墙”的玄学现象。元胞自动机(Cellular Automata)把空间切成网格,每个人占一个格子,按转移概率往邻居格移动,计算快、代码短,适合做大规模(几千人)的疏散时间统计,但轨迹是离散的,画出来的路径会有锯齿,论文里解释“行人避让细节”时说服力弱一些。离散事件仿真(DES)不关心连续空间,只关心“到达出口—排队—通过”这个事件序列,适合把疏散拆成“预动作阶段+移动阶段+排队阶段”做解析计算,优点是能直接套排队论公式,缺点是丢失了空间几何对拥堵的影响。我一般会这样选:题目给了详细平面图、要求分析拥堵机理,用社会力;题目只给总人数和出口总宽度、要求算疏散时间,用元胞自动机;题目要求做灵敏度分析、比较不同疏散策略的统计差异,用离散事件或元胞自动机跑多次取分布。代码包里如果三种都有,优先跑通和你选题最匹配的那一个,别贪多。
2.2 从代码包目录判断该改哪个文件
一个典型的疏散仿真代码包,目录结构通常长这样:models/下放social_force.py、cellular_automata.py,scenarios/下放classroom.json、stadium.json这类场景配置,utils/里是画图、统计、导出CSV的工具,根目录有run_simulation.py和requirements.txt。你第一步不是读所有代码,而是打开run_simulation.py,看它默认加载哪个场景、调用哪个模型、输出什么文件。常见做法是:先原封不动跑一遍,确认能生成trajectory.csv和evacuation_time.txt,再动参数。如果跑不起来,八成是依赖版本问题——numpy、matplotlib、scipy这三个库的版本冲突是血泪经验里排第一的翻车点。下面这段命令是我习惯的“最小验证”流程,先建虚拟环境再装依赖,避免污染系统Python:
python -m venv venv_evac source venv_evac/bin/activate # Windows下用 venv_evac\Scripts\activate pip install -r requirements.txt python run_simulation.py --scenario classroom --model social_force --steps 600逻辑说明:--scenario指定场景配置文件,--model指定模型,--steps是仿真步数(不是秒数,要看代码里每步代表多少秒)。参数说明:如果requirements.txt里写的是numpy>=1.20这种宽泛版本,建议手动锁到numpy==1.24.3、matplotlib==3.7.1,因为新版本numpy对np.float的弃用会让老代码直接报错。跑通之后,打开输出的trajectory.csv,看前几行是不是有id, x, y, vx, vy, time这些列,如果没有,说明代码包用的是元胞自动机,输出的是id, cell_x, cell_y, time。这一步确认完,你才知道后面该改哪些参数。
2.3 场景配置文件的参数含义与改法
场景文件(比如classroom.json)里通常包含:房间尺寸(room_width、room_height)、出口位置和宽度(exits列表,每个出口有x、y、width)、障碍物(obstacles,用矩形或多边形表示)、人员初始分布(initial_positions或spawn_area)、人员半径(radius,社会力模型里常用0.25~0.3米)、期望速度(desired_speed,正常步行1.2~1.4 m/s,紧急疏散可设1.5~2.0 m/s但别超过2.5,否则会出现不现实的“飞人”)。改参数时最容易翻车的是出口宽度:如果你把width从1.0改成2.0,但代码里计算排斥力时用的还是旧的网格步长,就会出现“人卡在出口两侧”的假拥堵。正确做法是改完width后,同步检查cell_size(元胞自动机)或interaction_range(社会力)是否匹配。我一般会做一个参数对照表,把题目里给的实测数据(比如“出口宽1.8米,人流密度0.5人/平米”)和代码里的变量一一对应,写进论文附录,这样评委能看到你的参数来源,而不是凭空捏造。
3. 把平面图变成仿真场景:坐标、网格与出口的落地操作
3.1 从图片到坐标:手动取点与脚本辅助
美赛题目给的平面图往往是PDF或图片,没有CAD坐标。你需要把关键点(墙角、出口端点、障碍物顶点)的像素坐标转成仿真用的米制坐标。常见做法是:用matplotlib打开图片,手动点几个已知尺寸的参照物(比如门宽0.9米对应多少像素),算出比例尺,然后写一个简单的取点脚本。下面这段代码是我常用的“点选取点”工具,运行后会弹出一张图,你依次点击出口两端和房间四角,它会打印出米制坐标:
import matplotlib.pyplot as plt import matplotlib.image as mpimg import numpy as np img = mpimg.imread('floor_plan.png') fig, ax = plt.subplots() ax.imshow(img) points = [] def onclick(event): if event.xdata and event.ydata: points.append((event.xdata, event.ydata)) ax.plot(event.xdata, event.ydata, 'ro') plt.draw() cid = fig.canvas.mpl_connect('button_press_event', onclick) plt.show() # 假设你已知门宽0.9米,点击了门两端,像素距离为pixel_dist # scale = 0.9 / pixel_dist (米/像素) # 后续所有像素坐标乘以scale即可逻辑说明:event.xdata和event.ydata是点击位置的像素坐标,points列表按点击顺序保存。参数说明:比例尺scale必须用题目里明确给出的尺寸来算,如果题目没给,就用“标准门宽0.9米”或“标准走廊宽1.8米”做参照,并在论文里注明假设。注意:不要直接用图片的像素坐标当米制坐标,否则仿真里人会小得像蚂蚁,出口排斥力完全失真。
3.2 出口与障碍物的建模:矩形、多边形与优先级
出口在代码里通常用线段或矩形表示。社会力模型里,出口是一个“吸引点”,人会朝出口中心移动,同时受到出口两侧墙的排斥力。元胞自动机里,出口是若干“目标格”,人进入目标格就算疏散成功。障碍物则用矩形或多边形,代码里一般有add_obstacle(x, y, width, height)或add_polygon(points)这样的接口。我踩过的一个坑是:把出口设在房间角落时,如果出口宽度等于墙厚,人会从墙的侧面“挤出去”,因为排斥力计算时只考虑了出口线段,没考虑相邻墙。解决办法是在出口两侧各加一个短墙段,或者把出口稍微内缩0.1米。另一个坑是障碍物重叠:如果你用两个矩形拼一个L形障碍物,重叠区域会导致排斥力翻倍,人会被弹飞。正确做法是用一个多边形表示L形,或者确保矩形不重叠。下面是一个场景配置的示例片段,展示出口和障碍物的写法:
{ "room_width": 20.0, "room_height": 15.0, "exits": [ {"x": 10.0, "y": 0.0, "width": 1.8, "direction": "south"}, {"x": 20.0, "y": 7.5, "width": 1.2, "direction": "east"} ], "obstacles": [ {"type": "rectangle", "x": 5.0, "y": 5.0, "width": 3.0, "height": 2.0}, {"type": "polygon", "points": [[12.0, 8.0], [14.0, 8.0], [14.0, 10.0], [12.0, 10.0]]} ], "agents": { "count": 200, "radius": 0.25, "desired_speed": 1.3, "spawn_area": {"x_min": 2.0, "x_max": 18.0, "y_min": 2.0, "y_max": 13.0} } }逻辑说明:exits里每个出口有位置、宽度和朝向,direction用于确定人朝哪个方向走。obstacles支持矩形和多边形,多边形按顶点顺序连接。agents里count是总人数,spawn_area是初始分布区域。参数说明:desired_speed单位是米/秒,radius单位是米,spawn_area要避开障碍物和墙,否则初始化时人就在墙里,仿真第一步就会报错。改完配置后,先跑一个--steps 10的短仿真,用matplotlib画出初始位置和出口,确认没有人在墙里、出口没被堵住,再跑完整仿真。
3.3 初始分布:随机、网格与真实人流密度
初始分布直接影响疏散时间的方差。随机分布(random)简单,但可能出现局部密度过高(比如200人挤在10平米里),导致仿真一开始就“炸开”。网格分布(grid)均匀,但不符合真实场景(人不会站成棋盘格)。我一般用“泊松盘采样”或“带最小距离的随机”,保证任意两人间距不小于2*radius。代码包里如果有spawn_agents函数,看它用的是哪种;如果没有,可以自己加一个拒绝采样循环:
import numpy as np def spawn_agents(count, area, min_dist, max_attempts=10000): agents = [] attempts = 0 while len(agents) < count and attempts < max_attempts: x = np.random.uniform(area['x_min'], area['x_max']) y = np.random.uniform(area['y_min'], area['y_max']) if all((x - ax)**2 + (y - ay)**2 >= min_dist**2 for ax, ay in agents): agents.append((x, y)) attempts += 1 if len(agents) < count: print(f"警告:只生成了{len(agents)}人,请扩大区域或减小min_dist") return agents逻辑说明:min_dist通常设为2*radius + 0.05,留一点余量。参数说明:max_attempts防止死循环,如果生成人数不够,说明区域太小或min_dist太大,需要调整。这个函数跑一次可能几秒钟,200人以内没问题;如果人数上千,建议用更高效的泊松盘算法或直接网格加扰动。初始分布确定后,在论文里放一张初始位置图,标注密度(人/平米),评委就能判断你的场景是否合理。
4. 跑仿真与调参:步长、速度、排斥力系数怎么设
4.1 时间步长与仿真步数的换算
社会力模型是连续时间微分方程,代码里用欧拉法离散化,dt(时间步长)通常取0.01~0.05秒。dt太大(比如0.1秒),人会“瞬移”穿过墙;dt太小(比如0.001秒),计算量翻十倍,但精度提升不明显。我一般取dt=0.02秒,然后--steps 30000对应600秒仿真时间。元胞自动机的时间步长是“每步每人移动一格”,代码里通常有step_time参数,表示每步代表多少秒,常见取0.5~1.0秒。如果你不确定代码里steps和秒数的关系,打开主循环,找time += dt或time += step_time这一行,自己算一下。论文里必须写清楚“仿真总时长XX秒,对应实际疏散时间XX秒”,否则评委无法判断你的结果是否收敛。
4.2 期望速度与人员构成:别让所有人都跑得一样快
真实疏散中,人员速度服从某种分布(比如正态分布,均值1.3 m/s,标准差0.2 m/s),而不是所有人同一个速度。代码包里如果给每个人赋相同的desired_speed,疏散时间会偏乐观(因为最慢的人被平均掉了)。我一般会改成从正态分布采样,并限制在0.8~2.0 m/s之间:
import numpy as np def assign_speeds(count, mean=1.3, std=0.2, vmin=0.8, vmax=2.0): speeds = np.random.normal(mean, std, count) speeds = np.clip(speeds, vmin, vmax) return speeds逻辑说明:np.clip防止出现负速度或超现实速度。参数说明:mean和std要根据题目给的数据或文献值来设,如果题目没给,用1.3和0.2并在论文里注明。另外,如果有“老人”“儿童”等特殊人群,可以单独设一组更慢的速度,比如0.6~1.0 m/s,然后看他们对总疏散时间的影响。这个改动很小,但能让你的灵敏度分析多一个维度。
4.3 排斥力系数:A、B参数的物理意义与调法
社会力模型里,人与人排斥力通常写成A * exp((r_ij - d_ij) / B),其中A是排斥力强度(牛顿),B是排斥力范围(米),r_ij是两人半径之和,d_ij是两人距离。A越大,人越早开始避让,但太大会导致“抖动”;B越大,排斥力作用距离越远,太大会让人在很远就绕路。常见取值:A=2000,B=0.08(Helbing经典论文里的值)。但代码包里可能用的是归一化后的无量纲参数,比如A=5.0,B=0.5。调参时,先跑一个小场景(比如50人、一个出口),观察轨迹:如果人挤在出口前不动,说明A太小或B太小;如果人还没靠近就互相弹开,说明A太大或B太大。我一般会固定B=0.08,然后以A=1000, 2000, 4000跑三次,看疏散时间的变化,选一个既没有“穿墙”也没有“冻结”的值。注意:改A和B后,要重新检查dt是否还稳定,因为排斥力太大会让欧拉法发散。
5. 避坑与排查:疏散仿真里最容易翻车的5个地方
5.1 现象:仿真跑着跑着人穿墙了
原因:时间步长dt太大,或者排斥力系数A太大,导致单步位移超过墙厚。解决:把dt降到0.01,或者把A减半,同时在墙的排斥力里加一个“硬约束”——如果人进入墙内,直接把它推回最近边界。代码包里如果有check_wall_collision函数,确保它在每一步都调用。
5.2 现象:疏散时间曲线一直不收敛,跑越久人越多
原因:出口的“通过判定”写错了,比如人到了出口但没被移除,或者出口宽度小于人的直径,导致人卡在出口。解决:检查出口判定条件,通常是if distance_to_exit < threshold: remove_agent。threshold要大于radius,比如radius+0.1。另外,出口宽度至少0.8米,否则单人通过都困难。
5.3 现象:初始位置生成失败,报“无法放置所有人员”
原因:spawn_area太小,或者min_dist太大,或者障碍物占用了太多区域。解决:先算一下可用面积除以(2*radius)^2,看理论上能放多少人。如果人数接近上限,改用网格加扰动,或者允许少量重叠但给一个初始排斥力让它们自己分开。
5.4 现象:换了场景文件后,仿真结果完全不对
原因:场景文件里的坐标单位不统一,比如房间尺寸用米,但出口坐标用像素。解决:在代码里加一个单位检查,或者统一在场景文件里用米,并在注释里写清楚。我习惯在run_simulation.py开头打印room_width和exits的坐标,跑之前肉眼扫一眼,确认数量级合理。
5.5 现象:论文里的图和代码输出的图不一致
原因:代码里用了随机种子,但没固定,每次跑结果不同;或者画图时用了不同的steps。解决:在代码开头设np.random.seed(42),并在论文里注明“随机种子固定为42,结果可复现”。画图时用同一份trajectory.csv,不要重新跑仿真。
6. 进阶技巧:用疏散仿真做灵敏度分析与论文图表
6.1 批量跑参数扫描:用循环代替手动改配置
美赛论文里如果只有一组结果,说服力有限。我一般会做参数扫描:出口宽度从0.8到2.4米,步长0.4米,每个宽度跑10次(不同随机种子),取疏散时间的均值和标准差,画误差棒图。下面是一个批量跑的脚本框架:
import subprocess import json import numpy as np widths = [0.8, 1.2, 1.6, 2.0, 2.4] results = {} for w in widths: times = [] for seed in range(10): # 修改场景文件 with open('scenario.json', 'r') as f: config = json.load(f) config['exits'][0]['width'] = w with open('scenario_tmp.json', 'w') as f: json.dump(config, f) # 运行仿真,假设输出到 evacuation_time.txt subprocess.run(['python', 'run_simulation.py', '--scenario', 'scenario_tmp.json', '--seed', str(seed)], check=True) with open('evacuation_time.txt', 'r') as f: times.append(float(f.read().strip())) results[w] = {'mean': np.mean(times), 'std': np.std(times)} print(f"宽度{w}m: 均值{results[w]['mean']:.1f}s, 标准差{results[w]['std']:.1f}s")逻辑说明:每次修改场景文件后跑一次仿真,收集疏散时间。参数说明:seed要传给仿真代码,确保每次随机种子不同但可记录。这个脚本跑50次可能花几十分钟,建议先在少量参数上测试。跑完后用matplotlib画width vs mean_time的折线图,加误差棒,论文里就能说“出口宽度从0.8米增加到2.4米,疏散时间下降约40%,但超过1.6米后边际收益递减”。
6.2 用热力图和密度图讲清楚瓶颈在哪
疏散仿真最有价值的输出不是总时间,而是“哪里堵了”。我习惯用matplotlib的hist2d或hexbin画人员密度热力图,时间窗口取疏散开始后的第10~20秒(拥堵最严重的时候)。代码大致如下:
import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv('trajectory.csv') # 取时间窗口 window = df[(df['time'] >= 10) & (df['time'] <= 20)] plt.hexbin(window['x'], window['y'], gridsize=30, cmap='Reds', mincnt=1) plt.colorbar(label='人数') plt.xlabel('x (m)') plt.ylabel('y (m)') plt.title('10-20s 人员密度分布') plt.savefig('density_heatmap.png', dpi=300)逻辑说明:hexbin把空间切成六边形网格,颜色越深表示该区域人数越多。参数说明:gridsize控制网格粗细,30~50比较合适;mincnt=1过滤掉空网格。这张图放进论文,配合出口位置的标注,就能直观看出“出口前3米范围内密度达到4人/平米,形成拱形拥堵”,比单纯写“疏散时间120秒”有说服力得多。
6.3 验证仿真是否可信:三个自检指标
跑完仿真后,别急着写论文。先做三个自检:第一,总疏散时间是否随人数线性增长?如果200人用60秒,400人用300秒,说明出口通过能力饱和,这是合理的;但如果400人用120秒,说明你的模型里人“叠”在一起了,密度计算有误。第二,最大密度是否超过物理极限?正常人密度上限约5~6人/平米,如果热力图显示10人/平米,说明排斥力参数需要调大。第三,出口流量是否合理?单出口宽度1米时,实测流量约1.2~1.5人/秒,如果你的仿真显示3人/秒,说明出口判定太宽松。这三个指标不用写进论文,但你自己得心里有数。我一般会在代码里加一行打印max_density和exit_flow,跑完扫一眼,不对就回去调参。希望帮到你。
本文还有配套的精品资源,点击获取