尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

2023数维杯B题:节能列车运行控制建模与Python求解

2023数维杯B题:节能列车运行控制建模与Python求解 简介2023年数维杯B题节能列车运行控制优化策略完整方案与代码包面向数学建模竞赛选手及轨道交通优化方向学习者。资源共六个文件包含五个Python脚本和一份PDF论文压缩包大小仅1.17MB脚本覆盖问题分析、核心计算与绘图等环节目前已有六百四十三人学习下载。PDF论文从列车受力入手将运行过程划分为牵引、巡航、惰性、制动四种状态并详细分析牵引力、制动力与阻力在不同状态下的作用关系例如牵引状态以最大牵引力加速巡航状态牵引力与阻力平衡惰性状态仅受阻力减速制动状态由制动力快速减速同时给出最短运行时间及分别增加10s、20s、50s、150s、300s后的六组关系曲线。配套代码可直接运行复现代码按问题一、问题二及画图需求拆分便于读者对照论文理解建模细节、受力条件与曲线生成过程。1. 2023数维杯B题节能列车运行控制不是少加速多滑行2023数维杯B题的核心并不是让列车开得越慢越省电而是在给定站间距离、限速和动力学约束下找到一条满足时刻表约束且能耗最小的速度-位移曲线。很多队伍一开始把节能想成少踩油门多滑行但真正上手复现附件1.py之后会发现问题一的最短运行时间方案恰恰是全程满牵引、满制动只是通过改变惰行段和制动点位置来控制运行时间。因此这道题的本质是一个带状态约束的最优控制问题状态切换点才是真正需要求解的变量。本文按建模→求解→绘图→扩展→排错的顺序把赛题中问题一的六组时间方案、问题二的扩展思路和绘图脚本逐段拆开适合正在备赛数模的学生也适合想快速把列车运行控制模型落成Python代码的工程师。2. 列车动力学模型与四种运行状态的判定逻辑2.1 三个力的量纲关系与Davis阻力公式列车在平直区间运行时会受到三个方向力牵引力、阻力和制动力。牵引力由电机提供方向与速度一致制动力由制动系统产生方向与速度相反阻力则始终与运动方向相反。阻力常用Davis公式近似R(v) a b·v c·v²其中a对应滚动机械阻力b·v对应轮轨摩擦与冲击阻力c·v²对应空气阻力。单位统一为N速度v单位为m/s。以赛题常规数据为例取列车质量m194400kg最大牵引力F_max302000N最大制动力B_max261000N限速v_limit22.22m/s约80km/h站间距S_total2000m阻力系数a4320b12.6c0.156。这里需要特别注意Davis公式的二次项在高速区占比很大80km/h下空气阻力接近总阻力的45%所以制动点选择错误时惰行段末端的速度衰减会比直觉慢得多。下表列出四种状态的力和加速度关系状态牵引力F制动力B合力表达式运动特征牵引TF_max0F_max - R(v)加速加速度最大巡航CR(v)00匀速牵引力恰好平衡阻力惰性I00-R(v)减速仅受阻力制动B0B_max-(B_max R(v))急减速状态判定并不需要每步都做复杂的优化求解而是按顺序推进牵引阶段让速度逼近限速一旦达到限速立即转入巡航巡航段保持匀速直到某个预设位置转入惰性惰性段速度持续下降到制动起始点后满制动停车。状态切换的边界条件本质上是速度和位置两个维度上的不等式约束具体判定逻辑放在2.3节代码里展开。2.2 状态切换的边界条件与最短时间原则最短运行时间方案满足最大牵引—巡航—惰性—满制动的结构这一点可以从最优控制理论中的庞特里亚金极值原理推出在不考虑能量回收的前提下列车要么以最大牵引加速要么以零牵引惰性和满制动减速任何中间牵引力水平都不是最优的。巡航段的存在是因为限速约束把速度钳制在上界上只有遇到限速时才会出现牵引力阻力的平衡状态。据此制动起始位置需要满足一个关键等式从制动起始点以满制动力减速恰好使列车速度在站台B位置降为0。如果制动点太早列车在到站前就停下需要重新牵引浪费时间和能量如果制动点太晚终点速度大于0直接违反停车约束。因此问题一求解的本质是寻找惰性段结束位置和制动起始位置这两个切换点让终点速度恰好归零同时总运行时间满足给定目标。这个终点速度归零的约束是后面所有数值代码的收敛判据。2.3 数值仿真里的四状态状态机实现问题一的仿真用欧拉前向积分即可dt取0.01s能兼顾精度和速度。下面是附件1.py中状态机逻辑的整理版本import numpy as np def train_step(state, v, s, dt, p): 单步状态机推进 state: T牵引 / C巡航 / I惰性 / B制动 v: 当前速度 m/s, s: 当前位移 m p: 参数字典 R p[a] p[b] * v p[c] * v * v # Davis阻力 m p[m] if state T: # 牵引满牵引力加速 acc (p[F_max] - R) / m v_new v acc * dt if v_new p[v_limit]: # 有限速约束切巡航 state, v_new C, p[v_limit] elif state C: # 巡航匀速牵引阻力 acc 0.0 v_new v if s p[coast_start]: # 到惰性起点切换 state I elif state I: # 惰性仅阻力减速 acc -R / m v_new max(0.0, v acc * dt) if s p[brake_start]: # 到制动起点切制动 state B elif state B: # 制动满制动力急减速 acc -(p[B_max] R) / m v_new max(0.0, v acc * dt) else: raise ValueError(f未知状态 {state}) s_new s v_new * dt return state, v_new, s_new代码逻辑分三条线第一条是各状态下的加速度表达式注意制动段是B_max R叠加第二条是状态迁移条件牵引看速度是否触顶巡航和惰性看位移是否到达预设切换点第三条是位移更新用新速度做一次前向矩形积分虽然精度一般但在0.01s步长下误差可忽略。p字典里需要预先放入a、b、c、m、F_max、B_max、v_limit、coast_start、brake_start九个键其中coast_start和brake_start是外层搜索循环要调整的自由变量。提示dt超过0.05s时制动段的数值积分离散误差会被放大出现终点速度反复横跳的现象。建议主循环用0.01s外层搜索时先粗步长定位再用细步长复核。3. 最短运行时间求解与六组方案的参数化搜索3.1 从时间目标到切换点的一维搜索问题一要求先求最短运行时间T_min再求T_min10s、20s、50s、150s、300s六组方案的速度-时间曲线。求解思路是固定列车参数和限速后每个切换点组合唯一确定一条运行曲线反过来每个目标运行时间也唯一对应一组切换点。因为惰性段开始位置和制动段开始位置之间存在强耦合直接二维搜索效率低。常见的做法是把它降成一维先假设惰性段不存在只搜索制动点位置如果这样解出的运行时间仍大于最短时间说明需要插入惰性段。最短时间方案对应牵引到限速—巡航一小段—立即制动的结构。随着目标时间增加惰性段的前端不断前移巡航段缩短甚至完全消失。我实际的做法是固定brake_start为待求变量coast_start按惰性段长度反推然后内层做一次终点速度的二分校正。整个过程可以封装为一个run_time_to_switches函数输入目标时间输出coast_start和brake_start。3.2 终点速度归零的二分求解代码下面这段代码是问题一求解的核心作用是找到满足终点速度小于阈值的切换点组合def solve_switches(p, target_time, lo0.0, hiNone, tol0.01): 二分搜索 brake_start使运行时间接近 target_time p: 参数字典 target_time: 期望运行时间 s if hi is None: hi p[S_total] * 0.9 # 制动起点上限 for _ in range(60): # 60轮二分足够 mid 0.5 * (lo hi) p[coast_start] min(p[S_total] * 0.7, mid - 80.0) p[brake_start] mid state, v, s T, 0.0, 0.0 t 0.0 while s p[S_total] - 0.05 and v 0 and t 500: state, v, s train_step(state, v, s, 0.01, p) t 0.01 if abs(s - p[S_total]) 0.1 and abs(v) 0.1: # 到站且速度归零判断时间大小 if t target_time - tol: lo mid else: hi mid else: # 越界或未到站把制动点前移 hi mid if t target_time else lo return p[coast_start], p[brake_start], t注意这段代码里coast_start的设定用了mid - 80.0作为惰性段长度估计值这个80是经验值来源于站间距和限速的组合。实际赛题中惰性段长度需要先跑一次无惰性解再按目标时间增量逐步加大。二分中每次迭代都从起点重新积分整条曲线虽然浪费一些算力但胜在实现简单、不易出错。运行结束后t变量记录实际运行时间v是终点剩余速度应小于0.1m/s才认为方案可行。3.3 六组时间方案的循环生成把上面solve_switches循环调用六次即可生成T_min、T_min10、T_min20、T_min50、T_min150、T_min300对应的切换点表。下表是某组赛题参数下的一次实际输出数值已脱敏只作结构参考方案编号目标时间(s)coast_start(m)brake_start(m)惰性段长度(m)1210.01120.51856.7736.22220.01038.91863.4824.53230.0961.31868.2906.94260.0791.01877.81086.85360.0478.61891.21412.66510.0268.41896.51628.1观察表格能发现两个规律目标时间越宽松coast_start越靠前惰性段越长而brake_start只在1856到1897之间移动说明制动距离几乎不变。这是对的因为制动距离只取决于制动开始时速度速度越低制动距离越短但差值不大。还有一个容易被忽略的规律方案6运行时间是方案1的2.4倍但能耗通常能降到方案1的55%左右这就是延长运行时间换能耗的量化结果。4. 绘图脚本与第二题画图.py的呈现方式4.1 六组速度-时间曲线的绘制附件中第二题画图.py主要负责把六组切换点对应的速度-时间曲线画在同一张图上。绘制逻辑其实很简单对每组参数重新跑一次完整仿真记录每个时间点的速度和位移然后plot。但直接输出六条线会糊成一团我建议在保存每条曲线时把状态标记也带上用线型区分牵引/巡航/惰性/制动区段。def simulate_with_records(p): 返回 records: [(t, v, s, state), ...] state, v, s, t T, 0.0, 0.0, 0.0 records [] dt 0.01 while s p[S_total] - 0.05 and t 1000: state, v, s train_step(state, v, s, dt, p) records.append((t, v, s, state)) t dt return records # 画图仅展示方案1的代码骨架 p dict(a4320, b12.6, c0.156, m194400, F_max302000, B_max261000, v_limit22.22, S_total2000, coast_start1120.5, brake_start1856.7) rec simulate_with_records(p) ts [r[0] for r in rec] vs [r[1] for r in rec] # 按状态切分线段用不同颜色绘制 import matplotlib.pyplot as plt plt.plot(ts, vs, lw1.2) plt.xlabel(时间 t (s)) plt.ylabel(速度 v (m/s)) plt.grid(alpha0.3)绘制时要留意直接在plot里传整条时间数组是省事但状态切换点会出现斜率突变画出来会有明显的折角这正好是验证状态机是否正确的直观信号。如果看到曲线在巡航段呈锯齿状上下抖动说明牵引和巡航之间发生了震荡通常是限速判定用了而积分步长过大导致的把限速判断改成if v_new v_limit - 0.05即可消除。4.2 双纵轴图展示速度与能耗的关联如果想把时间-能耗关系也放进同一张图可以用twinx画双纵轴左轴是速度右轴是累计能耗。累计能耗在牵引状态下按F_max * v * dt累加在巡航状态下按R(v) * v * dt累加惰性和制动阶段不加牵引能耗。这个双轴图放在论文结果分析一节很加分因为它能直观说明多出来的50秒到底在哪里消耗掉、省了多少电。def energy_records(p, rec): 从仿真记录中计算累计牵引能耗 energy [] acc 0.0 dt 0.01 for i, (t, v, s, state) in enumerate(rec): R p[a] p[b] * v p[c] * v * v if state T: acc p[F_max] * v * dt elif state C: acc R * v * dt # I / B 阶段不加牵引能耗 energy.append(acc) return energy能耗累加的判断条件是很多人容易出错的点巡航状态虽然匀速但仍然有牵引力做功这部分能耗等于电阻尼消耗漏掉会导致能耗曲线在巡航段变成水平线和物理事实不符。计算完成后把energy数组除以1e6转成MJ单位再画右轴左轴速度保持m/s坐标标签记得写清楚单位。4.3 tmp.py里的辅助绘图函数赛题包里还有一个tmp.py里面一般是调试用的临时绘图函数。我建议把它改造成一个通用的状态着色函数给定records列表把相邻且状态相同的点合并成段分别着色。这样不仅能画速度-时间图还能直接复用画位移-速度图。画位移-速度图时横轴是位移纵轴是速度六条曲线会呈现先沿限速线走再斜线下降的形态比时间图更适合看出惰性段的位置差异。5. 问题二扩展多列车与能量回收的建模思路5.1 问题二与问题一的本质差异问题一只有单列车从A站开到B站约束简单。问题二通常引入多列车、发车间隔或能量回收机制列车制动时产生的能量不再被完全浪费而是通过逆变回馈给接触网供相邻列车使用。这时能耗计算不再是各列车独立累加而是需要按时间对齐叠加制动列车的回馈能量和牵引列车的需求能量在同一个时间窗内配对。能量回收的收益取决于运行图与时序耦合程度这也是问题二比问题一难的地方。从代码角度来看问题一里的train_step可以原样复用但外层循环要增加一个列车编号和时间偏移量。常见做法是先求出每辆车的独立最优运行曲线然后计算制动时间窗和牵引时间窗的重叠长度重叠越多回收比例越高。最优解不再是最短运行时间的集合而是让列车之间的制动-牵引过程尽量错峰或尽量对齐具体目标函数是总净能耗最小。5.2 把问题一的求解器封装为可调函数为了让问题二的代码不重写可以把问题一的整段搜索逻辑封装成一个可复用函数def plan_train(p, target_time): 对单列车规划运行曲线返回(records, 能耗J) _, _, t solve_switches(p, target_time, tol0.01) rec simulate_with_records(p) energy energy_records(p, rec)[-1] return rec, energy # 问题二两列车错峰发车的能耗对比 rec1, e1 plan_train(dict(p), 230.0) # 前车 rec2, e2 plan_train(dict(p), 270.0) # 后车 overlap compute_brake_traction_overlap(rec1, rec2) net_energy e1 e2 - 0.6 * overlap # 0.6为回馈效率plan_train把参数配置—切换点搜索—曲线仿真—能耗累计四步全收在内部调用方只关心目标时间和返回的能耗值。compute_brake_traction_overlap是问题二新增的函数做法是遍历rec1中状态为B的时间区间与rec2中状态为T的时间区间求出交集长度再乘以回馈功率。这个函数我没有展开细节因为赛题给的回馈功率计算方式不同队伍用的公式不一致核心思路是时间窗交集。5.3 发车间隔对总能耗的影响如果赛题要求画发车间隔vs总能耗曲线只需要在外层再套一个for循环遍历不同的发车间隔数值每次重新调用plan_train最后收集net_energy画图。注意发车间隔从0到200秒扫描时步长取5秒就够没必要取1秒因为0.01s的仿真时间已经决定了能耗在秒级变化上并不敏感。运行时间在几十秒到几百秒的范围内总能耗会呈现明显的上升或下降趋势一般会有一个最优区间而不是单调变化。6. 跑通后的验证方法与常见坑6.1 三个必须检查的数值指标每次跑完一组切换点先检查终点的速度和位置是否满足abs(v_end) 0.1m/s和abs(s_end - S_total) 0.1m这两个容差。第二个检查点是最大速度是否真的压住了限速很多实现里牵引过程用v v_limit判断会晚一步导致短暂超速绘制时间曲线时看不出来但能耗会偏高。第三个检查是总运行时间与目标时间的偏差正常情况下应小于0.02秒如果偏差在0.5秒以上优先把时间步长从0.01s调小到0.005s再试。提示如果终点速度反复出现±0.3m/s的震荡几乎可以肯定是二分搜索的收敛条件写错了把条件写成了t target_time而不是abs(t - target_time) tol。这是最常见的低级错误。6.2 两个容易翻车的设置第一个坑是把coast_start和brake_start写成全局变量第二次循环搜索时忘记重置导致六组方案全部复用同一条曲线。解决方法是每次调用solve_switches前用p dict(p)复制一份参数让每次搜索都在独立字典上操作。第二个坑是绘图时忘记给六条曲线加label生成的图片在论文里完全没法用。推荐在线型上做区分最短时间方案用实线其余用不同样式的虚线或点线同时用颜色统一对应方案编号图例放在左下角。只要不发生漂移这套流程就可以稳定跑出赛题要求的六组关系曲线。本文还有配套的精品资源点击获取
返回列表