ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

MATLAB轨道交通仿真系统:从列车运行到客流交互的完整实现

MATLAB轨道交通仿真系统:从列车运行到客流交互的完整实现 简介轨道交通仿真系统代码包面向城市交通规划、轨道运营优化及仿真建模技术人员用于模拟列车从启动、加速到减速停车的完整过程并还原乘客上下车与换乘行为。代码将线路条件、信号系统、牵引供电等复杂因素统一建模其中“duringpw7”标签对应工作日17—19时高峰时段可针对高负荷条件下的运营瓶颈进行分析。压缩包共20个文件以11个m脚本为核心涵盖主控流程、站点距离计算、速度更新、位置展示等算法模块5个mat数据文件保存站点坐标与乘客分布png图与fig界面辅助可视化txt则提供说明整体仅107KB轻量紧凑。已有419人学习适合需要构建或改进仿真模型的开发者也能帮助研究者深入理解数据结构与调度逻辑为发车间隔优化、站台布局调整及应急预案制定提供数据支撑。1. 轨道交通仿真系统一套能跑通列车运行全过程的MATLAB代码拆开这个压缩包的时候我第一反应是找 main.m 和 run.m。对于一个列车运行仿真程序没有这两个入口文件后面十几个 m 文件都是黑匣子。这套代码的定位很明确用 MATLAB 实现列车从启动、加速、匀速、减速到停车进站的全过程同时把站点客流生成和上下车行为也塞进了同一个循环里而不是像很多论文代码那样只给一个孤零零的运动学模型。如果你在做轨道交通相关的课程设计、毕设或者想快速搭一个能展示“列车在线上跑、站点有人在等车”的仿真 Demo这套代码是很好的起点。它不涉及牵引供电和信号系统的细节那些在仿真里被简化成了速度约束和停站时间正好把注意力集中在列车运行和客流交互这两条主线上。2. 拆包看架构这套仿真系统由哪几部分组成运行主流程怎么走2.1 文件清单与模块分工代码不是一堆散文件而是三层结构拿到压缩包先别急着点 run.m我第一次跑这类仿真就吃过亏——文件都没认全就运行报错连哪个函数抛的都分不清。把文件按职责过一遍这套代码的结构其实很清晰。层级文件职责入口层main.m、run.mmain.m 负责初始化环境和加载数据run.m 负责仿真主循环逻辑层getSpeed.m、getDistance.m、updatePosition.m列车速度计算、步长位移计算、位置更新客流层getRandSitePeopleNum.m按站点和时间生成随机候车人数显示层siteStateDisplay.m、trainPositionDisplay.m、updatePlotPlotPosition.m、plotCircle.m、resetSiteStateDisplay.m站点状态显示、列车位置显示、绘图位置刷新数据层data.txt、siteDistance.mat、site_pos_x_y.mat、site.PNG 等线路静态数据、站点坐标、底图分层的好处是改参数不用翻逻辑代码。想调客流动 getRandSitePeopleNum.m想改线路动 data.txt 和 mat 文件想换显示效果碰显示层那五个函数。逻辑层和显示层是分开的这正是我判断这套代码“能跑通也改得动”的依据。2.2 从 main.m 到 run.m先理清主流程再动手改参数一般这类仿真项目的主流程是main.m 负责把站点坐标、距离矩阵、底图全部 load 进来然后调用 run.m 进入循环。run.m 内部就是标准的“时间步推进→算速度→算位移→更新列车→更新站点→刷新画面”直到仿真时钟走完。% run.m 仿真主循环骨架按模块名整理的常见结构 load(siteDistance.mat); % 站点间距离矩阵 load(site_pos_x_y.mat); % 站点坐标数组 totalTime 3600; % 仿真总时长单位秒 dt 1; % 时间步长单位秒 time 0; while time totalTime v getSpeed(time, pos); % 取当前时刻、当前区间的速度 d getDistance(v, dt); % 由速度和时间步长算位移 updatePosition(pos, d); % 更新列车在某条线路上的逻辑位置 sitePeople getRandSitePeopleNum(pos, time); % 生成当前站客流 trainPositionDisplay(pos); % 刷新列车在底图上的位置 siteStateDisplay(sitePeople); % 刷新站点候车人数显示 drawnow; time time dt; end这段代码里有几个参数直接决定仿真分辨率dt 设 1 秒一小时的仿真要循环 3600 次画面刷新基本流畅如果 dt 设 0.1 秒精度上去了但图形刷新会明显变卡我一般先跑 1 秒步长确认逻辑无误再缩步长。getSpeed 的返回值单位要注意和 dt 匹配如果速度返回的是 m/s、dt 是秒getDistance 算出来的就是米。2.3 数据文件是“静态底图”siteDistance.mat 和 site_pos_x_y.mat 存了什么仿真里最容易被忽略的就是那几个 mat 文件。siteDistance.mat 我推测是一个 n×n 的站点间距离矩阵第 i 行第 j 列代表站点 i 到站点 j 的线路距离site_pos_x_y.mat 是 n×2 的坐标数组每一行是某个站点在底图上的 x、y 像素坐标或逻辑坐标。这两个文件是配套的坐标决定了站点画在哪距离矩阵决定了列车跑多久到站。% 查看数据文件结构 load(siteDistance.mat); load(site_pos_x_y.mat); whos siteDistance site_pos_x_y; size(siteDistance) % 应该是 n x n size(site_pos_x_y) % 应该是 n x 2运行这段代码如果发现 siteDistance 是 5×5、site_pos_x_y 是 6×2那说明站点数对不上列车跑起来不是越界就是停在站外。这种问题后面避坑章节会重点讲。data.txt 则是文本格式的线路信息常见做法是每行一个站点字段包含站点编号、坐标和是否换乘站标记方便人工修改后重新生成 mat 文件。3. 核心算法逐个拆列车位置、速度与站点状态的更新逻辑3.1 getSpeed 和 getDistance速度与里程是怎么算出来的这两个函数是整套仿真里跟运动学关系最紧密的部分。getSpeed 的核心逻辑通常是查表或按区间返回一个预设速度——有的实现是给每个区间一个最高限速列车在该区间内匀速行驶有的实现会分阶段起步阶段速度线性增长、接近站点前线性减速。我在类似项目里见过最常见的写法是后者因为这样能比较自然地模拟启动和制动过程。% getSpeed.m 常见实现按区间和时间阶段返回速度 function v getSpeed(time, pos) % pos 为列车当前位置这里按三段式加速-匀速-减速 if time 10 % 启动加速阶段10秒内线性加到30m/s v 3 * time; elseif time 50 v 30; % 中间匀速跑40秒 else v 30 - 3 * (time - 50); % 最后10秒减速到0 end end这个函数的关键参数是加速时间和最高速度。把加速阶段从 10 秒改成 20 秒曲线更平缓但同样距离下平均速度会下降。getDistance 就简单多了本质是 v 对 dt 的积分如果 getSpeed 返回的是当前速度、dt 是时间步长位移就是 v × dt如果是分段速度函数更稳妥的做法是在每个 dt 内取首尾速度的平均值再乘 dt这样加速阶段误差小很多。% getDistance.m用梯形法算一步位移比直接 v*dt 更稳 function d getDistance(v, dt) persistent vLast; if isempty(vLast) vLast v; end d (v vLast) / 2 * dt; % 梯形积分兼顾加减速过程 vLast v; end这里用了一个 persistent 变量保存上一步速度换来的是加减速阶段位移更平滑。如果你把 dt 设得很大比如 5 秒梯形法比矩形法的优势会非常明显。3.2 updatePosition 和 updatePlotPosition列车位移与图形刷新的关系updatePosition.m 负责更新列车在“逻辑空间”里的位置——也就是沿着线路累计走了多少米updatePlotPosition.m 则负责把逻辑位置转换成底图上的显示坐标。这一步是很多初学者容易绕晕的地方逻辑位置是标量沿线的里程显示坐标是二维坐标底图上的 x、y中间需要一个映射关系。最常见的做法是先算列车当前处于哪两个站点之间再用线性插值把里程比例映射到两个站点坐标之间% updatePlotPosition.m逻辑位置到显示坐标的映射 function [x, y] updatePlotPosition(pos, sitePos) % sitePos 为 n x 2 的站点坐标数组 % pos 为沿线路累计里程 segment floor(pos / distancePerSegment) 1; ratio (pos - (segment - 1) * distancePerSegment) / distancePerSegment; x sitePos(segment, 1) * (1 - ratio) sitePos(segment 1, 1) * ratio; y sitePos(segment, 2) * (1 - ratio) sitePos(segment 1, 2) * ratio; end这里 distancePerSegment 表示一个区间的长度如果你的线路各区间长度不一致就不能用这种固定值除法得先用站点累计里程数组去查 pos 落在哪个区间。参数上最容易踩的坑是 sitePos 的坐标顺序要和 distance 矩阵的顺序一致否则列车会在底图上“跳线”。3.3 trainPositionDisplay 与 siteStateDisplay列车和站点的可视化逻辑trainPositionDisplay.m 负责把列车画出来常见做法是在底图 site.PNG 上用 plot 函数画一个矩形或圆代表列车siteStateDisplay.m 则负责把每个站点的候车人数画出来人数多就把标记点画大或者加文字标注。这两个函数通常会配合 plotCircle.m 使用——画站点圆圈、画列车占位。resetSiteStateDisplay.m 则是每一轮仿真开始前把显示状态清零的函数防止上一轮残留的图形污染新一轮数据。% resetSiteStateDisplay.m重置图形句柄的典型结构 function resetSiteStateDisplay() % 删除上一轮所有列车和客流图形对象 delete(findobj(Type, patch)); delete(findobj(Type, text)); drawnow; end参数层面trainPositionDisplay 接收的通常是列车逻辑位置和底图句柄siteStateDisplay 接收的是站点人数数组——一个长度为站点数的向量。你在改这两个函数的时候只要保证“逻辑层函数只管算数、显示层函数只管画图”就不会把仿真逻辑和界面渲染搅在一起。4. 把参数改成你自己的数据文件、发车逻辑与客流模拟的调整方法4.1 改线路基础数据从 data.txt 到重新生成 siteDistance.mat这套代码默认带了一条线路和一组站点坐标但你迟早要换成自己的线路数据。第一步是改 data.txt。我一般会把 data.txt 设计成每行一个站点站点编号、x 坐标、y 坐标、是否换乘站用空格或逗号分隔。改完之后不能直接跑因为 siteDistance.mat 里还是旧的距离矩阵。% 重新生成站点距离矩阵 data load(data.txt); sitePos data(:, 2:3); % 取 x、y 坐标 n size(sitePos, 1); siteDistance zeros(n, n); for i 1:n for j 1:n siteDistance(i, j) sqrt((sitePos(i,1)-sitePos(j,1))^2 ... (sitePos(i,2)-sitePos(j,2))^2); end end save(siteDistance.mat, siteDistance); save(site_pos_x_y.mat, sitePos);这里用的是欧氏距离。如果你的线路是弯曲的比如绕山或者走地下弧线欧氏距离就不够用了得从你的线路纵断面数据里提取实际里程放到距离矩阵里。我见过比较省事的做法是在 GIS 里量好各站间的实际里程直接填进去不走坐标反算。这个 mat 文件和 data.txt 的一致性决定了后面所有仿真结果的可信度。4.2 getRandSitePeopleNum 的三种改法固定值、均匀随机、时段分段这个函数的返回值决定了每个站点在每个仿真时刻有多少人在等车。原代码里大概是生成一个随机整数但实际用途不同三种改法各有适用场景。% 改法一固定客流用于验证运动学逻辑排除随机因素 function people getRandSitePeopleNum(siteId, time) people 20; % 每个站永远20人等车跑出来的数据才方便对比 end固定值的好处是结果完全可复现适合先验证列车运行部分有没有 bug坏处是乘客行为完全没有随机性研究站台拥挤度就不合适。% 改法二均匀随机客流基础随机场景 function people getRandSitePeopleNum(siteId, time) people randi([5, 30]); % 每站5到30人随机 end均匀随机适合平峰场景每个站点、每个时刻的客流都在同一水平线上波动。注意 randi 的上下限单位是“人”不是“人/分钟”这个值代表的是当前时刻站台上的存量候车人数。% 改法三早晚高峰分段贴合 duringpw7 场景 function people getRandSitePeopleNum(siteId, time) % time 为秒17*3600 表示下午5点19*3600 表示晚上7点 if time 17 * 3600 time 19 * 3600 people randi([30, 80]); % 高峰时段客流密度翻倍 if siteId 3 % 假设站点3是换乘站 people round(people * 1.5); end else people randi([5, 20]); end end这个分段逻辑就是“duringpw7”这个标签最常见的落法——把 17:00 到 19:00 作为晚高峰窗口在窗口内提高客流生成上下限。如果你做的是一整天的仿真就把这个判断改成 7×3600 到 9×3600 早高峰、17×3600 到 19×3600 晚高峰两个窗口。timeSec 用秒做单位是这类仿真里的共识避免直接在分钟和小时之间来回换算出错。4.3 高峰时段仿真怎么搭记录站点滞留人数来评估瓶颈改完客流生成你已经可以在 duringpw7 窗口里跑出高峰期客流了。但光看到站台人数不停涨还不够你得记录数据才能看出哪些站是瓶颈。在主循环里加一段记录代码。% 在 run.m 主循环里添加记录逻辑 peopleRecord zeros(totalTime / dt, n); % 每时刻每站人数 for i 1:n peopleRecord(timeIdx, i) getRandSitePeopleNum(i, time); end % 仿真结束后统计每个站的最高滞留人数 [maxPeople, peakTimeIdx] max(peopleRecord);这段代码把每个时刻、每个站点的候车人数全存下来了。跑完 duringpw7 的仿真那个 maxPeople 值最大的站点就是压力最大的站——如果它还是个换乘站基本可以确认瓶颈出在运力配置而不是站点布局上。这个记录数组是你后面做运营策略对比的唯一依据。5. 避坑与排查MATLAB 跑这套代码最容易翻车的五个地方5.1 load 找不到 mat 文件路径没设对报错一片红现象运行 main.m 直接报错类似 “Unable to open file siteDistance.mat”函数名还没开始跑就挂了。原因MATLAB 的工作目录不在代码所在文件夹load 只能找当前路径和搜索路径下的文件。你从压缩包解压后如果没把当前文件夹切过去直接双击 m 文件运行路径就是错的。解决在 MATLAB 里先用 cd 切到解压目录或者右键文件夹选择“添加到路径”。我习惯在 main.m 顶部写死这两行。% main.m 顶部强制切到脚本所在目录 cd(fileparts(mfilename(fullpath))); addpath(pwd);5.2 距离矩阵和坐标矩阵对不上列车直接“飞”出底图现象画面里列车不在线上跑而是跑到底图外或者沿着乱七八糟的折线跳动。原因siteDistance.mat 是 n×nsite_pos_x_y.mat 是 n×2但这两个 n 不是同一个值。最常见的是 data.txt 你加了一个站点坐标数组重新生成了距离矩阵没重新生成或者距离矩阵是旧的 5 站版本坐标是新 6 站版本。解决先用第 2 章里的查看代码检查两个文件维度是否一致。不一致就重新生成 siteDistance.mat不要手工改 mat 文件。5.3 矩阵索引越界updatePlotPosition 报 index exceeds matrix dimensions现象仿真跑到第几步突然报错列车位置在站点之间但索引却指到了 n1 行。原因列车越过了最后一个站点还在继续累加里程updatePlotPosition 里的 segment 1 超过了 sitePos 的行数。这通常是因为线路终点没有设一个停站判断或者最后一个区间的距离算错了。解决在主循环里加一个终止检查。列车的 pos 上限是线路总长一旦 pos 总长就强制置为总长并结束本次运行。if pos totalLineLength pos totalLineLength; break; % 列车到终点退出当前循环 end5.4 循环里画面卡死图形句柄没有复用每一帧都是新画现象仿真能跑但画图极慢一秒的仿真时间在屏幕上要卡好几秒甚至直接卡住没响应。原因不少初学者在 updatePlotPosition 里每次都调一遍 plot而不是用 set 更新已有图形对象的 XData、YData。每帧创建一个新图形对象几千帧下来内存里堆了几千条线不卡才怪。解决在循环外先创建图形对象用 h plot(x, y, ro); 拿到句柄循环里只调 set(h, XData, x, YData, y)。这在 trainPositionDisplay 里是一个关键优化点。% trainPositionDisplay.m 里用句柄更新位置而不是重复 plot h plot(0, 0, ro, MarkerSize, 10); % 只创建一次 % 进入循环后 set(h, XData, trainX, YData, trainY);5.5 随机客流结果无法复现每次仿真的乘客数都不一样现象同一套参数前后跑两次仿真结果完全不同想对比某两个运营方案的效果根本分不清是方案差异还是随机波动。原因getRandSitePeopleNum 里用了 randi而 MATLAB 的随机数种子每次启动都是不同的等价的实验条件跑不出等价的数据。解决在 main.m 或 run.m 开头调用 rng 设置固定种子。这是我跑仿真实验前必做的一步。rng(20240516); % 固定随机数种子保证实验可复现种子只要设一次整个工作区的随机数序列就固定了。想换一组随机数据改种子数字就行。这个习惯能帮你省掉大量“这个结论到底可不可信”的争论。5.6 MAT 文件版本太新老版本 MATLAB 读不了现象你用 MATLAB R2023a 保存的 siteDistance.mat拿到 R2016a 上一跑load 报错提示文件是 v7.3 格式无法读取。原因文件较大或用了高版本保存选项时MATLAB 默认存成 HDF5 格式v7.3老版本不支持。解决save 的时候显式指定版本。我自己发的仿真代码包里通常会写成 save(siteDistance.mat, siteDistance, -v7); 这样在 R2010 以后的所有版本都能读兼容性最好。6. 进阶把“看一眼”变成“留数据”给你的仿真加个结果记录器前面第 4 章提过在循环里记录站点人数这里说一个更完整的做法把列车位置、站台人数、当前速度一起记下来仿真结束后导出 CSV。有了 CSV你就可以用表格软件直接画曲线不用每次都在 MATLAB 里重新跑图。具体做法是开两个矩阵一个记录时间序列数据一个记录每站客流快照。列车位置、速度这些是时间步级别的数据客流是站点级别的数据两者维度不同不要硬塞进同一个矩阵。% run.m 里的数据记录器完整示例 simLen floor(totalTime / dt); timeSeries zeros(simLen, 3); % 第1列时间第2列位置第3列速度 peopleSnap zeros(simLen, n); % 每一行是某时刻各站人数 idx 0; while time totalTime idx idx 1; v getSpeed(time, pos); d getDistance(v, dt); updatePosition(pos, d); timeSeries(idx, 1) time; timeSeries(idx, 2) pos; timeSeries(idx, 3) v; for s 1:n peopleSnap(idx, s) getRandSitePeopleNum(s, time); end time time dt; end % 仿真结束导出 CSV T array2table(timeSeries, VariableNames, {TimeSec, PositionM, SpeedMps}); writetable(T, train_position.csv); P array2table(peopleSnap, VariableNames, ... arrayfun((k) sprintf(Site_%d, k), 1:n, UniformOutput, false)); writetable(P, site_people.csv);这里把每个时间步的位置和速度存成 train_position.csv各站人数存成 site_people.csv。打开后你可以看到通过 duringpw7 时段仿真得到的客流曲线——如果某一条曲线在 18:00 前后冲高之后一直不回落说明站台上的人持续积压发车间隔就该加密了。我从这套代码里学到最实在的一条是无论项目多小先跑一版固定客流、固定速度的基线数据再叠加随机因素对比永远比直接上随机参数更稳。那段时间我每次加模块都强制自己先跑一遍空载基线再开始加线路、加客流。后来做别的仿真项目这个习惯也一直留着省过不少跟数据打架的时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表