简介:这套MATLAB程序包与《交通流建模》一书配套,面向交通工程、智能交通领域的初学者和研究者,旨在借助运行代码消除交通流理论与仿真实践之间的鸿沟,适合课程设计、毕业设计阶段动手验证三参数关系与不同模型特点。压缩包内含16个m文件,整体仅12KB,属于轻量级代码集,按功能划分出基本图绘制、宏观LWR与微观跟驰仿真、元胞自动机规则、参数初始化、坐标后处理及扰动加载等模块,可独立运行或配合教材逐段调试。目前已有282人在线学习浏览。通过实际操作,学习者既能掌握流量-密度-速度三参数关系的可视化方法,也能对比宏观模型、元胞自动机模型和微观跟驰模型的适用场景,同时培养数据预处理、仿真结果分析与信号配时等控制策略验证能力,可作为后续研究或课程设计的起点。
1. 交通流建模 MATLAB 程序包:宏观守恒方程与微观跟驰模型的同台复现
赶过晚高峰的人几乎都见过导航地图上的深红色路段:明明没有事故,车流却走走停停,最后又莫名恢复。这类现象正是交通流建模要解释的核心。Traffic Flow Modelling 是交通工程里把宏观连续模型、微观跟驰模型讲得比较系统的经典书,这套 MATLAB 程序就是它的配套代码。压缩包内有 17 个 .m 文件,ESM 是场景入口,CASEPARS、MODELPARS、NUMPARS、PLOTPARS 四组参数文件分别描述路网、模型、数值格式和绘图配置,TrafficFlowSimulation.m 是主循环,TimeStepMacro.m 和 TimeStepMicro.m 分别负责宏观与微观推进。也就是说,拿到代码后不用从零搭仿真器,只要读参数、理顺主循环,就能复现 LWR 密度波,也能看到微观跟驰模型里的走走停停。适合正在啃交通流理论的学生、要做仿真的智能交通从业者,以及习惯用 MATLAB 复现论文的工程师。
2. 从 CASEPARS 到 TrafficFlowSimulation:参数文件与仿真主循环的装配方式
2.1 四组参数文件各自管什么
这套程序的第一道门槛不是公式,而是参数文件。ESM 没有 .m 后缀,第一次读代码时容易被忽略。我的习惯是先看 ESM,再看四组 PARS 文件,最后打开 TrafficFlowSimulation.m。
| 参数文件 | 管什么 | 典型字段举例 |
|---|---|---|
| CASEPARS.m | 场景与边界条件 | roadLength, numLanes, initialDensity, boundaryType, bottleneckPosition |
| MODELPARS.m | 车辆行为模型 | modelType, freeSpeed, rhoJam, s0, tau, maxAccel |
| NUMPARS.m | 离散化与数值格式 | numCells, dx, dt, simTime, sampleEvery |
| PLOTPARS.m | 输出与可视化 | plotMode, colorRange, movingRefV |
CASEPARS 管“在哪里仿”,MODELPARS 管“车怎么跑”,NUMPARS 管“算得稳不稳”,PLOTPARS 管“看得出看不出”。把场景和模型参数分开后,同一个路网可以直接切换宏观、微观模型,不需要改道路长度和边界条件。NUMPARS.dt 是宏观程序里最敏感的参数:如果freeSpeed * dt / dx超过 1,守恒格式就会失稳;微观程序则不受这个约束,主要看每辆车每个时间步的位移是否超过前车间距。
2.2 ESM 如何拼装参数并触发初始化
ESM 的等效脚本片段如下。它没有像函数一样被call,而是按脚本方式把四组参数放进当前工作区,再调用初始化函数。
% ESM 场景装载脚本(伪代码,按该文件实际工作方式整理) CASEPARS; % 载入场景参数 MODELPARS; % 载入模型参数 NUMPARS; % 载入数值参数 PLOTPARS; % 载入绘图参数 state = Initialization(CASEPARS, MODELPARS, NUMPARS); [state, logs] = TrafficFlowSimulation(state, CASEPARS, MODELPARS, NUMPARS, PLOTPARS);这段代码展示的是一条完整数据流:四组参数进入工作区,Initialization 根据宏观还是微观创建初始状态,TrafficFlowSimulation 返回更新后的 state 和日志 logs。实际函数签名可能和我写的不完全一致,但数据走向是这条线。四个 PARS 文件共用一套结构体命名,省去了传递大量单独变量的麻烦;代价是读代码时要随时注意字段来源,比如CASE.numVehicle和NUM.numCells都可能存在,前者用于微观初始化,后者用于宏观网格。
Initialization 里最常做的事情是构造一个非均匀初始密度。下面这段等价逻辑可以用来制造初始排队段:
% Initialization.m 中可能出现的密度初始化 x0 = linspace(0, CASE.roadLength, NUM.numCells)'; rho0 = CASE.initialDensity * ones(NUM.numCells, 1); % 在道路 30%~50% 区间把密度翻倍,模拟初始拥堵 idx = round(NUM.numCells * 0.3) : round(NUM.numCells * 0.5); rho0(idx) = rho0(idx) * 2; state.x = x0; state.rho = rho0; state.v = MODEL.freeSpeed * ones(NUM.numCells, 1);逻辑说明:把道路离散成NUM.numCells个格子,再在中间段设置一个高密度块。这样宏观程序一开始就会向右释放消散波、向左释放冲击波,不需要额外做扰动。微观初始化更简单,一般是等间距放置车辆,再给同一个初始速度。参数说明:initialDensity的单位要和roadLength、numCells对齐;如果写成辆/km 而道路长度用米,算出来的车辆数会差三个量级,这是最常见的初始化错误。
2.3 TrafficFlowSimulation.m 的主循环骨架
宏观和微观的推进函数差别很大,但主循环骨架是统一的:初始化、步进、按采样周期记录日志。
% TrafficFlowSimulation.m 主循环骨架 for it = 1 : NUM.simTime / NUM.dt if strcmp(MODEL.type, 'macro') state = TimeStepMacro(state, CASEPARS, MODELPARS, NUMPARS); else state = TimeStepMicro(state, CASEPARS, MODELPARS, NUMPARS); end if mod(it, PLOTPARS.sampleEvery) == 0 logs.t(end + 1) = it * NUM.dt; logs.rho(end + 1, :) = state.rho; logs.v(end + 1, :) = state.v; end end逻辑说明:TimeStepMacro和TimeStepMicro都接收 state 和全部参数,返回更新后的 state。PLOTPARS.sampleEvery决定日志密度,设为 1 表示每个时间步都记录,适合短时仿真;对微观程序来说,由于时间步长通常远小于宏观,建议设 5 或 10,否则内存里会堆满大矩阵。这里有一个常见边界问题:NUM.simTime / NUM.dt不一定是整数,直接用for it = 1 : N会把尾步丢掉。我一般会改成while t <= NUM.simTime,每步更新t = it * NUM.dt,保证最后一段时刻也被记录。
3. FundamentalDiagram.m 与 TimeStepMacro.m:LWR 守恒方程的数值落地
3.1 三参数关系与 FundamentalDiagram.m
交通流三参数关系是判断程序是否跑对的第一道标准。宏观模型里最基本的满足关系是q = ρ * v,也就是流量等于密度乘以速度。常见线性速度-密度关系为v = vf * (1 - ρ / ρjam),代入后得到抛物线的流量-密度关系。实际高速路基本图更像三角形,因为自由流段和拥堵段的斜率不同,但教科书配套程序往往先用线性关系讲原理。
% FundamentalDiagram.m 中 Q-K 关系绘制核心部分 rho = linspace(0, 200, 200); % 密度轴(辆/km) vf = 80; % 自由流速度(km/h) rhoJam = 200; % 阻塞密度(辆/km) v = vf * (1 - rho / rhoJam); % 线性速度-密度关系 q = rho .* v; % 流量 = 密度 * 速度 plot(rho, q, 'LineWidth', 1.5); xlabel('密度 K (辆/km)'); ylabel('流量 Q (辆/h)'); grid on;逻辑说明:当密度为零时流量为零,当密度等于阻塞密度时速度为零、流量也为零,最大流量出现在rho = rhoJam / 2处,对应临界密度。程序里可以把这个理论曲线保存下来,再把仿真输出的密度-流量散点叠上去,基本图上出现明显偏离就说明初始场、边界或数值格式有问题,这是最直接的自检方式。
| 三参数关系 | 表达式 | 关键含义 |
|---|---|---|
| 速度-密度 V-K | v = v(ρ) | 密度升高,速度下降 |
| 流量-密度 Q-K | q = ρ v(ρ) | 临界密度处流量最大 |
| 流量-速度 Q-V | q = ρ(v) v | 高速低流、低速高流两个分支 |
3.2 TimeStepMacro.m 的迎风差分实现
LWR 守恒方程可以写作∂ρ/∂t + ∂q/∂x = 0。离散时不能直接用中心差分,因为交通波有明确方向:信息从上游传向下游,所以要用迎风格式取上风侧流量。
% TimeStepMacro.m 中的一阶迎风更新片段 function state = TimeStepMacro(state, CASE, MODEL, NUM) rho = state.rho; v = Speed(rho, MODEL); % 平衡速度 q = rho .* v; % 当前流量场 % 迎风:取上游格点流量作为当前格点边界流量 q_flux = [q(1); q(1:end-1)]; dqdx = (q - q_flux) / NUM.dx; rho_new = rho - NUM.dt * dqdx; % 物理约束:密度不能越界 rho_new = max(0, min(CASE.rhoJam, rho_new)); state.rho = rho_new; state.v = Speed(rho_new, MODEL); end逻辑说明:q_flux把每个格点的上游邻居当作入流流量,这是典型的一阶迎风。rho_new = rho - dt * dqdx就是守恒方程显式时间推进。密度截断保证数值解始终落在物理区间内。这个格式能复现激波,但会有数值耗散,激波前沿会被抹平一点;要减小耗散,可以把q_flux换成 Godunov 精确黎曼解或者加 Minmod 斜率限制,本质都是更准确地计算网格边界处的流量。
参数说明:稳定性要求满足 CFL 条件,也就是max(v) * dt / dx <= 1。这里max(v)直接取MODEL.freeSpeed即可,不需要在运行时算最大值。如果看到密度出现负值或激波前方振荡,先看NUM.dt是否偏大,再看边界条件是否写对。闭环道路必须做周期边界,也就是最后一个格点的上游邻居要回到最后一个或第一个格点,不能简单补零;开环道路则要给定入口流量,否则车辆在边界处会凭空消失。
3.3 Speed.m 与 Position2Spacing.m 的角色差别
Speed.m在宏观和微观两个分支里的含义不同。宏观分支输入密度,输出平衡速度;微观分支输入车头间距,输出期望速度。Position2Spacing.m则是把位置向量转成车头间距数组,为微观模型准备数据。下面这种写法把两类模型统一到一个接口里:
% Speed.m 的等价实现 function v = Speed(rhoOrSpacing, MODEL) if strcmp(MODEL.type, 'macro') v = MODEL.freeSpeed * max(0, 1 - rhoOrSpacing / MODEL.rhoJam); else v = MODEL.freeSpeed * (1 - exp(-rhoOrSpacing / MODEL.s0)); end end逻辑说明:宏观分支里max(0, ...)保证密度超过阻塞密度后速度也不会变负;微观分支用指数松弛表示间距越大越接近自由流速度。两个分支都依赖MODELPARS里的参数,所以同一个Speed.m可以被宏观和微观两个推进函数调用。Position2Spacing.m则不同:如果输入是宏观格点位置,输出平均间距;如果输入是微观车辆位置,输出相邻车辆的车头间距。宏观里平均间距约等于1/ρ,微观里需要减掉车长才是有效间距,这个差别很容易在跨模型对比时踩坑。
4. TimeStepMicro 与 DoDisturbance:微观跟驰模型如何复现走走停停
4.1 微观状态量更新:从间距到速度再到位置
微观模型的状态量很简单:每辆车有位置x、速度v,车辆之间通过车头间距互相影响。TimeStepMicro.m每次推进分两步:先算期望速度或加速度,再更新速度和位置。下面是一段等效的松弛跟驰模型实现。
% TimeStepMicro.m 的车辆循环等效代码 function state = TimeStepMicro(state, CASE, MODEL, NUM) x = state.x; % 单车位置向量 v = state.v; % 单车速度向量 nv = length(x); % 车辆数 spacing = Position2Spacing(x, nv); % 车头间距 v_new = zeros(nv, 1); for i = 1 : nv s = spacing(i); vDes = MODEL.freeSpeed * (1 - exp(-s / MODEL.s0)); % 期望速度 a = (vDes - v(i)) / MODEL.tau; % 松弛加速度 v_new(i) = max(0, v(i) + a * NUM.dt); % 速度更新 end state.v = v_new; state.x = x + v_new * NUM.dt; % 位置更新 state.spacing = Position2Spacing(state.x, nv); end逻辑说明:这里用的是松弛跟驰模型,核心是期望速度公式vDes = vf * (1 - exp(-s / s0))。当间距很小时,期望速度接近零,车辆减速;当间距足够大时,期望速度接近自由流速度。MODEL.tau是松弛时间,决定驾驶员把速度调整到期望速度的快慢。注意位置更新用的是更新后的v_new,相当于一种半隐式处理,比用旧速度更不易出现车辆重叠。
参数说明:s0通常取 5~20 m,tau取 1~3 s。tau太小时车辆起步过猛,tau太大时车流重新加速很慢,正好对应走停状态里“消散慢”的过程。微观程序的NUM.dt通常取 0.01~0.1 s,比宏观小一个量级,所以总步数会多很多。
4.2 DoDisturbance.m:在车队中部注入一次速度扰动
微观仿真的难点不是让车流跑起来,而是制造出真实的扰动。DoDisturbance.m在某个时间窗口内压低某辆车速度,相当于人为制造一次刹车灯波。
% DoDisturbance.m 的等效实现 function state = DoDisturbance(state, it, CASE, MODEL, NUM) t = it * NUM.dt; if t >= CASE.disturbTimeStart && t <= CASE.disturbTimeEnd idx = round(length(state.x) * CASE.disturbPositionRatio); % 把第 idx 辆车限制到低速,模拟前车短暂减速 state.v(idx) = min(state.v(idx), CASE.disturbSpeed); end end在主循环里调用时,要和TimeStepMicro配合:
if strcmp(MODEL.type, 'micro') state = DoDisturbance(state, it, CASE, MODEL, NUM); state = TimeStepMicro(state, CASE, MODEL, NUM); end逻辑说明:扰动位置disturbPositionRatio通常取 0.5~0.7,不要放在头车。放在头车只会让单车减速,放在车队中部会让后车因为间距变小而逐辆减速,同时前车逐渐拉开,形成局部密度升高并向上游传播。若扰动窗口持续 3~5 s,模拟的是刹车灯波;若持续到仿真结束,模拟的是持续瓶颈。注意DoDisturbance之后要立刻执行一次TimeStepMicro,不要连续调用两次扰动,否则同一时刻速度会被压两次。
4.3 固定坐标与移动坐标:两种看拥堵演化的方式
微观输出是每个时刻的所有车辆位置,直接看轨迹图容易眼花。这套程序提供了两个观测视角:固定坐标和移动坐标。PlotFixedCoordinates.m用于固定路面位置,观察密度或速度随时间变化;PlotMovingCoordinates.m把坐标系以参考速度v_ref平移,方便跟踪密度波传播。
% PreprocessPlotMovingCoordinates.m 的核心变换 function xm = PreprocessPlotMovingCoordinates(x, t, v_ref) xm = x - v_ref * t; % 伽利略变换到移动坐标系 end然后可以用散点图绘制移动坐标下的轨迹:
% PlotMovingCoordinates.m 绘制移动坐标下的轨迹点 scatter(xm, t, 6, v, 'filled'); % 颜色表示速度 xlabel('移动坐标 x - v_{ref} t (m)'); ylabel('时间 t (s)'); colorbar;逻辑说明:v_ref选在自由流速度附近时,自由行驶的车辆轨迹接近竖直;走停状态下的减速波会变成明显斜线,因为波速通常低于自由流速度。如果所有轨迹都向同一个方向倾斜,说明v_ref选得不合适,可以取扰动窗口内波速的平均值重新变换。固定坐标绘图则更简单,先把车辆位置插值到规则网格上,再用imagesc画时空图;PreprocessPlotFixedCoordinates.m做的主要就是这种插值,网格间距太大会抹掉短时拥堵,太小又会放大插值噪声。
5. ESM 场景切换与守恒检验:把程序改造成参数实验平台
5.1 用 ESM 做批量场景扫描
ESM 最大的价值是场景可复用。把 ESM 放在一个批量脚本里循环执行,就能对反应时间、自由流速度、扰动窗口等参数做扫描。注意 MATLAB 的脚本循环里变量会残留,跑下一组场景前要重新加载四组 PARS。
% runExperiments.m 简单批量扫描 reactTimes = [0.5, 1.0, 2.0]; for i = 1 : length(reactTimes) ESM; % 载入默认场景 MODELPARS.tau = reactTimes(i); % 覆盖反应时间 state = Initialization(CASEPARS, MODELPARS, NUMPARS); [state, logs] = TrafficFlowSimulation(state, CASEPARS, MODELPARS, NUMPARS, PLOTPARS); results(i).tau = reactTimes(i); results(i).meanFlow = mean(logs.q(end - 200 : end)); end这里ESM每次都会重新执行,变量名相同所以会覆盖上一次的MODELPARS,这是脚本式参数装配的副作用。如果要跑更严格的实验,建议把 ESM 改成函数[CASE, MODEL, NUM, PLOT] = SetupScenario(scenarioID),返回参数结构体而不是往工作区塞变量。
5.2 用守恒律验证数值解
闭环道路下车辆数应该守恒。宏观程序里车辆总数等于密度对空间的积分,也就是sum(rho) * dx。每跑完一步都检查这个值,如果漂移超过千分之一,说明边界或差分格式有问题。
% 闭环边界下车辆数守恒检查 totalVehicle = sum(state.rho) * NUM.dx; if abs(totalVehicle - totalVehicle0) > totalVehicle0 * 1e-3 warning('车辆数不守恒: %.4f -> %.4f', totalVehicle0, totalVehicle); end微观模型没有这个守恒量问题,因为车辆是离散对象,只要不超越前车,总数就不变;但微观程序要检查最小间距是否小于车长,否则说明发生了碰撞。
5.3 用“扰动窗口提前 20 秒”验证冲击波速度
这套程序里最有意思的验证方式是只改CASE.disturbTimeStart,其他参数不动。把扰动窗口提前 20 s,再分别用固定坐标和移动坐标画两张时空图,对比两次仿真的密度波前沿位置。你会看到拥堵排队头的反向传播速度基本不变,这个速度在 LWR 模型里对应特征速度,在排队论里对应冲击波速度。若用移动坐标系把v_ref设成这个反向传播速度的绝对值,密度波前沿会变成一条接近竖直的线,说明坐标变换和波速测量都对上了。这一步跑通之后,这套交通流 MATLAB 程序就不再是“别人的代码”,而是你可以自由改参数、做实验、出图写结论的仿真平台。
本文还有配套的精品资源,点击获取