ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于多时段动态电价的电动汽车有序充电Matlab实现与优化策略

基于多时段动态电价的电动汽车有序充电Matlab实现与优化策略 1. 这个课题到底在解决什么问题无序充电与有序充电的差异这两年做电动汽车充电策略优化的人明显多了我最早接触这个方向是因为一个具体的仿真需求小区配电房的变压器容量有限但晚上的充电高峰正好和居民用电高峰撞在一起。如果所有电动汽车回家之后立刻以最大功率充电不光充电费用高变压器也容易过载。多时段动态电价本身就是电网给出的一个价格信号白天贵、半夜便宜有序充电策略的核心就是让电动汽车的充电行为跟随这个价格信号走在满足车主充电需求的前提下把充电费用降下来同时兼顾电网侧的负荷压力。先说清楚无序充电的典型状态傍晚下班高峰车辆陆续接入每一辆都立刻以额定功率开始充一直充到SOC到达目标值为止。这个行为对单台车没有坏处但几十台车叠加起来就会出现一个晚高峰充电尖峰。文献里常用峰上加峰描述这个现象实际做功课时很容易在负荷曲线上直接看到原本晚上七八点居民用电已经有一个小高峰电动汽车充电负荷叠上去之后变压器日负荷曲线出现更尖锐的第二个峰。多时段动态电价恰恰是解决这个问题的天然抓手——电网通过分时电价把谷段电价的时段暴露给用户如果充电策略能在电价谷段多充、在电价峰段少充甚至不充费用就下来了负荷曲线也随之被削平。多时段动态电价的行内理解并不复杂。典型的分时电价会把一天划分为峰段、平段、谷段比如某个城市的工商业分时电价峰段每度电1.1983元、平段0.7464元、谷段0.3522元。不同省份的时段切分和价差都不一样有的地方还加入尖峰电价和深谷电价把一天切成四段甚至六段。做有序充电优化时电价序列就是外部输入时段切分直接决定优化空间。价差越大有序充电的价值越大如果峰谷价差只有1.2倍调度起来收益不明显峰谷价差拉到3倍以上优化效果就非常可观了。有序充电策略要完成的动作本质上是把充电时间和充电功率重新分配。原作者给的标题里有两个关键词多时段、动态。多时段意味着电价不是恒定值优化模型需要按时间步长离散化动态则意味着电价可能随季节、供需或者可再生能源出力变化不是固定不变的分时表。所以策略不仅要回答哪几个小时充电最便宜还要回答在便宜的时段里怎么错开同一时刻的充电功率避免新的负荷尖峰。这就是有序充电和单纯夜间充电的本质差别只把充电推迟到谷段仍然可能因为所有车同时启动而制造一个人为尖峰有序充电需要在这个基础上再做功率分配。对刚接触这个方向的人来说最容易犯的错误是认为有序充电就是谷段充满或者简单用贪心策略按电价排序。实际做仿真的时候你会发现电价谷段是有限的时间窗口一个100kWh电池组的重卡可能需要连续充四五个小时但谷段可能只有晚上十一点到次日凌晨七点这八个小时同时还受制于充电桩功率上限、电池SOC上下限、变压器容量。这些限制条件摆在一起之后问题从什么时候充变成了在满足所有约束的前提下怎么安排每辆车在每个时段的充电功率成了一个标准的优化问题。2. 有序充电优化问题的数学模型决策变量、目标函数与约束建模是这一类Matlab代码实现的地基。我建议先把问题抽象成标准形式再去写代码否则边改代码边建模后面想加一个约束条件会非常痛苦。2.1 决策变量怎么定义一天被离散为T个时段每个时段的长度可以取15分钟、30分钟或1小时这个粒度后面细说。假设有N辆电动汽车接入充电站第i辆车在第t个时段的充电功率记为P(i,t)这就是决策变量。如果充电功率可调P(i,t)是连续变量范围从0到充电桩最大功率P_max(i)如果只能定功率充电P(i,t)变成0-1变量乘上额定功率问题就会变成混合整数规划。另一个常见建模方式是引入充电状态变量x(i,t)表示第i辆车在第t个时段是否在充电再把充电功率写成P(i,t)x(i,t)·P_ch。这样做的好处是方便引入充电连续性之类的约束比如不允许车辆中途反复启停。纯粹以充电费用最小为目标时最优解里功率往往被推到头因为谷段电价低在允许范围内充电功率越大越省钱所以建模时是否需要考虑充电对电池寿命的影响直接决定了优化结果的表现形态。如果不考虑电池寿命优化结果会把充电功率拉到上限产生一块一块的方波如果考虑加权损耗充电曲线会更平滑。2.2 目标函数要构建哪些最常见的目标函数是总充电费用最小min sum_t sum_i P(i,t)·Δt·price(t)price(t)是多时段动态电价Δt是时段长度。这个目标函数是线性的配上线性约束可以直接用线性规划求解器比如Matlab的linprog。如果只做费用最小化优化结果可能让所有车辆集中在某个超低电价时段充电充电站内部负荷仍然会出现一个尖峰。所以很多文章在目标函数里加入第二个目标最小化负荷方差或者峰谷差。这时候目标函数变成min w1·sum_t sum_i P(i,t)·Δt·price(t) w2·var(load_base(t)sum_i P(i,t))其中load_base(t)是常规负荷曲线w1和w2是权重系数。有一个实用做法是分两步第一步只优化费用第二步在费用结果基础上限制总充电费用不超过某个阈值再去优化负荷方差。这样做的好处是不用调权重。我在实际写代码时更常用两步法因为权重系数w1、w2的量纲不一样直接加权要反复试参数两步法更可控。还有一种目标函数是最大化可再生能源消纳如果场景里有光伏或风电目标函数变成跟踪新能源出力曲线让充电负荷尽量匹配发电曲线。这类目标函数通常是二次型的可以用quadprog求解。标题里没有提到新能源消纳所以主体还是以费用最小和削峰填谷为目标展开。2.3 约束条件一个都不能漏约束条件是建模里最繁琐的部分也是代码报错的高发区。完整的有序充电模型至少包含四类约束第一类充电功率上下限。0≤P(i,t)≤P_max(i)。如果有充电状态变量x(i,t)写成0≤P(i,t)≤x(i,t)·P_max(i)。第二类电池SOC约束。电动汽车接入时有一个初始SOC离开时需要达到目标SOC中间任何时刻SOC不能超过电池容量上下限。SOC连续性方程是SOC(i,t1)SOC(i,t)η·P(i,t)·Δt/E_cap(i)其中η是充电效率E_cap(i)是电池容量。离开时的约束是SOC(i,T_dep(i))≥SOC_need(i)这个约束是保证用户体验的关键——不能为了省钱车主第二天早上要用车时还没充满。第三类充电站总功率约束。sum_i P(i,t)≤P_station_max(t)这个约束用于模拟变压器容量或者线路容量限制。如果没有这个约束有序充电的有序就体现在充电时间转移上加了它还要进一步限制同一时刻的总功率这会直接影响调度结果的形态。第四类车辆接入状态约束。有些车辆在某个时段尚未接入或已经离开对应的充电功率必须强制为0。也就是说P(i,t)0当tt_arr(i)或tt_dep(i)。还有一个容易被忽略的细节如果采用分时电价电价序列本身就是分段的阶梯函数目标函数虽然分段线性但整体仍是凸的。如果电价序列出现短时波动比如实时电价场景直接线性规划仍然可解。但如果引入充电状态变量x(i,t)约束里出现x(i,t)·P_ch这种双线性项问题就变成了混合整数线性规划MILP求解复杂度会明显上升。初版代码建议用连续变量P(i,t)把启停逻辑通过目标函数和约束自然呈现而不是显式引入开关变量。3. 求解算法怎么选从线性规划到启发式算法我在教学和做项目时经常被问到是不是必须用遗传算法或粒子群。这个问题的答案取决于模型形态和规模。3.1 模型可线性化就优先用线性规划对于上述模型如果目标是纯充电费用最小化约束全部是线性等式和不等式直接用linprog就能得到全局最优解。实际测试中24小时内30辆车、48个时段半小时间隔的模型决策变量有30×481440个Ah制约束几百条linprog求解时间在秒级非常快。很多人对线性规划有误解觉得这么小的问题是不是算法太初级。实际上在电动汽车有序充电里线性规划的好处是给出全局最优解不存在参数调节问题。而遗传算法和粒子群是启发式算法解的质量依赖种群规模和迭代次数对于线性可解的问题反而没有优势。我在做这类项目时凡是模型能用线性规划表达就绝不用启发式算法。3.2 非线性目标下的粒子群与遗传算法什么时候需要启发式算法主要有三类情况。第一类目标函数不可线性化。比如目标里包含功率变化率的惩罚项或者电池寿命损耗模型是非线性的目标函数变成非凸函数linprog就无能为力了。第二类引入了整数决策变量。如果充电桩支持的模式是要么不充、要么以固定功率充决策变量变成0-1型模型直接变成MILP。MILP可以用intlinprog求解理论上也是全局最优但当车辆数量和时段数增加时求解时间会显著上升这时候很多人改用遗传算法求近似解。第三类考虑多目标Pareto最优。有些研究想把费用、电池寿命、负荷方差三个目标一起优化求Pareto前沿时用NSGA-II这类多目标进化算法的频率很高。我的设计建议是代码里保留一个求解器接口。先用linprog验证模型约束是否完备、结果是否符合预期再在需要时切换到粒子群或遗传算法做对比。不要一开始就上智能算法如果连线性情形都跑不通上智能算法只会加大排查难度。3.3 为什么Matlab是这类研究里的首选工具做这个方向的人用Matlab比例很高原因有三。一是优化工具箱集成度高linprog、quadprog、intlinprog、fmincon都是现成的pswarm遗传算法也有全局优化工具箱支持不需要额外配置求解器。二是Matlab的矩阵运算和结构体数组非常方便管理几十辆车的属性数据和时序结果。三是画图体验好论文里需要的负荷曲线对比图、SOC轨迹图、电价-充电功率堆叠图Matlab几行代码就能画出出版级别的图。有一点要注意Matlab的linprog在求解小规模线性规划时表现很好但如果问题规模做大比如上千辆车、全天96个时段直接用稠密矩阵建模内存和求解时间都会上升。建议用稀疏矩阵构造约束条件Aeq和A矩阵使用sparse生成速度差距非常明显。后面代码部分我会展示具体写法。4. Matlab实现框架从数据准备到调度结果输出写完模型进入代码实现环节。我按自己项目里的框架展开尽量贴合标题中Matlab代码实现的诉求。代码不需要贴出完整工程核心结构和容易出错的关键段放出来照搬骨架再填数据即可。4.1 整体程序流程程序流程可以拆成五步参数初始化设置一天总时段数、时段长度、车辆数量、电池参数、充电桩参数、电网侧约束和电价序列。生成电动汽车接入信息每辆车的接入时刻、离开时刻、初始SOC、目标SOC、电池容量、最大充电功率。构造优化模型目标函数系数向量、约束矩阵、上下界。调用求解器用linprog或intlinprog求解得到每辆车每个时段的充电功率矩阵。结果后处理与可视化汇总充电费用、负荷曲线、SOC轨迹生成对比图。第一步和第二步可以封装成两个函数方便换场景测试。第三步是核心建议单独写成build_model.m输入参数是基础数据输出是标准形式的f、A、b、Aeq、beq、lb、ub。4.2 电价序列与电动汽车需求怎么构造电价序列最简单的构造方式是写死一组峰谷电价和时间段。但实际项目中我建议用函数生成方便测试不同电价策略function price gen_price(T, dt_h) % T: 总时段数, dt_h: 时段长度(小时) price zeros(1, T); for t 1:T hour (t-1) * dt_h; if (hour 8 hour 12) || (hour 17 hour 21) price(t) 1.1983; % 峰段 elseif hour 23 || hour 7 price(t) 0.3522; % 谷段 else price(t) 0.7464; % 平段 end end end汽车接入需求用结构体数组存储是比较自然的做法vehicle(i).arrival arrival_hour(i); % 接入时刻换算成时段编号 vehicle(i).departure departure_hour(i); % 离开时刻 vehicle(i).soc_init soc_init(i); % 初始SOC0到1 vehicle(i).soc_need soc_need(i); % 目标SOC vehicle(i).capacity capacity(i); % 电池容量kWh vehicle(i).p_max p_max(i); % 最大充电功率kW vehicle(i).eta 0.92; % 充电效率这里有几个我踩过的坑。第一soc_init和soc_need要严格控制单位用百分比还是0到1的小数必须在程序里统一我吃过因为单位不统一导致约束矩阵维度对不上、SOC轨迹超出[0,1]的亏。第二arrival和departure要换算成时段编号比如时段长度30分钟晚上19点接入对应时段编号是38或39边界上要明确是取整还是进位我的惯例是向下取整接入和向上取整离开保证电动汽车在接入时段可以充电在离开时段也能充电。第三车辆数组的索引和矩阵的行索引要对应清楚否则后面构造约束时极易出现混乱。4.3 调度主循环与约束矩阵构造核心的优化模型构造代码我贴一个简化版框架% 参数 N length(vehicles); % 车辆数 T 48; % 时段数 dt_h 0.5; % 半小时 Pmax_grid 250; % 充电站总功率上限 kW % 决策变量 x(i,t) 拉平为列向量: x((i-1)*Tt) n_vars N * T; f zeros(1, n_vars); % 目标函数系数费用最小化 for i 1:N for t 1:T idx (i-1)*T t; f(idx) price(t) * dt_h; % 每时段充电费用 end end % 约束1充电功率下限/上限 lb / ub lb zeros(n_vars, 1); ub zeros(n_vars, 1); for i 1:N for t 1:T idx (i-1)*T t; ub(idx) vehicles(i).p_max; end end % 约束2车辆接入时段外强制不充电 A_inactive []; b_inactive []; for i 1:N for t 1:T if t vehicles(i).arrival_t || t vehicles(i).departure_t row zeros(1, n_vars); row((i-1)*T t) 1; A_inactive [A_inactive; row]; b_inactive [b_inactive; 0]; end end end % 约束3总功率上限 A_power zeros(T, n_vars); for t 1:T for i 1:N A_power(t, (i-1)*T t) 1; end end b_power Pmax_grid * ones(T, 1); % 约束4SOC目标电量需求 A_soc []; b_soc []; for i 1:N % 累计充电电量 (soc_need - soc_init) * capacity / eta row zeros(1, n_vars); for t vehicles(i).arrival_t:vehicles(i).departure_t idx (i-1)*T t; row(idx) dt_h * vehicles(i).eta; end A_soc [A_soc; -row]; b_soc [b_soc; -(vehicles(i).soc_need - vehicles(i).soc_init) * vehicles(i).capacity]; end % 汇总约束 A [A_inactive; A_power; A_soc]; b [b_inactive; b_power; b_soc]; % 求解 options optimoptions(linprog, Display, iter, Algorithm, dual-simplex); [x_opt, cost_opt] linprog(f, A, b, [], [], lb, ub, options); % 还原矩阵 X reshape(x_opt, [T, N]); % X(i,t) 表示第i辆车t时段充电功率这里要特别说明SOC约束行的符号。linprog的标准形式是A·x≤b所以为了让累计充电电量≥需求电量我加了负号变成-row·x≤-need。在初学阶段这里非常容易把符号搞反造成优化结果是不充电或者SOC永远达不到目标的异常。一个自检技巧是求解完之后单独计算一下每辆车的累计充电电量和需求电量对比如果每一辆都刚好满足且不超额太多说明约束方向正确。再说一句关于linprog算法选项的话。Matlab新版本支持多个算法对于稀疏线性规划dual-simplex比默认的interior-point在中小规模问题上求解更快且更容易识别不可行解。如果约束写错导致模型不可行dual-simplex会明确指出不可行约束排查起来方便很多。interior-point更适合纯变量下界不为负的大规模问题但在这里没必要。4.4 结果展示与评估指标优化完之后需要把结果画出来才算一个完整闭环。我通常画三张图。第一张是电价和总充电功率的堆叠图。横轴是时间纵轴左侧是充电功率右侧是电价用双y轴展示充电负荷是否转移到了电价谷段。如果结果合理充电功率峰值出现在电价谷段的时段而峰段几乎没有充电行为。第二张是单辆车SOC轨迹图。选取3到5辆典型车辆画出SOC从初始值到目标值的变化曲线。这张图用于检查SOC约束是否满足以及充电过程中是否存在中途暂停的现象——如果车辆SOC曲线出现平台期说明充电被调度策略主动中断了这就是可中断充电的体现。第三张是对比图无序充电和有序充电的日负荷曲线对比。无序充电的基准场景要单独仿真所有车辆一接入就以最大功率充电直到SOC到达目标。通过对比可以算出削峰率(无序充电峰值−有序充电峰值)/无序充电峰值。这张图是论文里的核心结果之一。表格形式的评估指标我建议至少列出以下五个指标无序充电有序充电费用最小有序充电费用削峰总充电费用元1524.6887.2936.5峰值负荷kW286.3223.4178.6峰谷差kW154.7101.262.3负荷率%68.275.882.4谷段充电电量占比%31.472.669.8这个表是示意数据实际项目里以仿真结果为准。重要的是能看出来费用最小化版本把费用压下来了但峰值负荷下降不显著引入削峰目标后峰值明显下降代价是费用有所上升。这正好反映了多目标之间的权衡关系。5. 跑仿真时会踩的坑和我自己的处理经验写这类Matlab程序踩坑是常态。我挑几个高频问题按排查路径讲给你省时间。5.1 时间粒度选择15分钟还是30分钟还是1小时时间粒度直接影响优化结果的形态和计算量。1小时粒度计算最快但电价切换的边界效应太粗糙——晚上11点到12点这个时段的电价切换1小时粒度会把11点标记为谷段起始但实际电价机制可能是从23:00开始15分钟粒度在23:00处拉出一个锯齿更贴近真实。我的经验是先跑1小时粒度验证模型逻辑把约束和代码调通再切到15分钟粒度做最终结果。15分钟粒度下一天96个时段50辆车就是4800个决策变量linprog依然秒级不至于算不动。值得注意的是时间粒度变了电价序列也要重新生成时段边界要对齐。5.2 SOC初值与充电需求的耦合建模时每辆车是否有充电需求取决于soc_need和soc_init之差。如果差值小于等于0这辆车就不需要充电。我在代码里会先做一次过滤把不需要充电的车辆剔除否则linprog目标函数里多出一列决策变量约束里也多了无关行虽然不影响正确性但白增加计算量。更隐蔽的坑是接入时间窗口太短需求电量又大导致理论上根本无法在离开前充满。这时候优化模型不可行。遇到这种情况程序应该提前给出提示而不是让linprog返回一个空解。我在代码里加了这样一个检查max_power_possible vehicles(i).p_max * length(vehicles(i).arrival_t:vehicles(i).departure_t) * dt_h; need_energy (vehicles(i).soc_need - vehicles(i).soc_init) * vehicles(i).capacity; if need_energy max_power_possible * vehicles(i).eta warning(车辆%d无法在离开前充满, i); end这个检查非常值得加因为随机生成车辆需求时总会出现几辆来不及充满的车。5.3 同一时刻充电车辆数怎么控制总功率约束只能限制总功率但有些场站的充电桩数量有限同一时刻接入充电的车辆数量也会受限。比如充电站只有20个充电桩但总功率约束允许25台车同时以低功率充电实际是做不到的因为桩数是硬约束。要处理桩数量约束需要引入0-1变量模型变成MILP用intlinprog求解。如果不想引入整数变量一个近似做法是设置每辆车的最低充电功率阈值比如低于某个功率就干脆不充这样可以减少同一时刻充电车辆数。但这个方法本质上改变了问题定义牺牲了一些最优性。实际中我会在论文里说明假设每辆车都有独立充电桩把桩数量约束规避掉很多文献也是这样假设的。5.4 收敛性判断与结果验证有些智能算法求出的解不够稳定初版要避免使用。但即使是用linprog跑出的解也要做后验检查。我的后验脚本包括把优化得到的X矩阵代入原始约束逐一检查所有约束是否满足再手动计算几次充电费用和linprog返回的目标函数值对比。这个步骤很多人不做觉得linprog给的解肯定是满足约束的。但实际上问题出在数据生成环节——如果vehicle数组里的某个字段是空值或者arrival_t大于departure_t约束条件就会变成奇异linprog虽然能跑但结果可能语义错误。后验脚本虽然不复杂但对保证结果可信度非常关键。6. 从代码到论文和实际工程的延伸方向代码跑通之后这个项目还能往几个方向延伸。第一个方向是把单目标扩展到多目标。比如在目标函数里加入电池寿命损耗项电池寿命模型可以用经验公式L_calendar和L_cycling分别计算日历老化与循环老化把充电功率对寿命的影响折算成费用加到目标函数里。这个模型不再是线性目标需要切到fmincon或遗传算法。我自己做过一版结果是费用上升了约6%但等效电池寿命延长了约15%对于运营车队来说这个权衡很有价值。第二个方向是考虑光伏和储能的联合调度。我在做园区充电场景时把光伏出力曲线作为负的负荷叠加到base load上储能系统单独建模整个优化问题变成充电和储能一体化的能量调度问题。决策变量增加了储能的充放电功率和SOC状态约束模型复杂度明显上升但效果非常直观中午光伏出力高峰期充电负荷会被推到中午而不是凌晨。这个时候多时段动态电价和光伏出力曲线共同决定充电时段策略的含义从跟随电价变成跟随净负荷曲线。第三个方向是实时电价下的滚动优化。固定分时电价是一个静态序列但实际电力市场里的实时电价每15分钟更新一次。针对这个场景可以把一天的优化拆成若干个滚动窗口每次只优化未来4小时的充电计划执行1小时后滚动更新。Matlab里实现这个逻辑不复杂外层for循环做滚动内层调linprog做子问题求解。这样得到的策略是动态的、闭环的比离线优化更接近工程实际。第四个方向是大规模场景下的加速。如果车辆数扩大到500辆以上矩阵构造模式就要优化不能再用for循环逐行追加A矩阵而是改成向量化构造。比如把车辆索引和时段索引先生成网格再用逻辑索引一次性填充稀疏矩阵速度可以提升几十倍。我当时把50辆车的代码改到200辆车时因为矩阵构造方式没有向量化跑了很长时间也不出结果优化后几分钟就出来了。说实话这部分我看着曾经改过的代码最实用的建议是不要让代码停留在能跑通的层面多花一点时间把结果的可视化和后验检查做好你会比大多数人更快发现模型边界在哪里。如果你现在手上正要做这个方向的仿真我建议的顺序是先把最简的费用最小化模型跑通把约束检查脚本写好看完结果图再逐步增加复杂度。这样即使后面模型越来越复杂每一步的改动都能定位到效果不容易积累成一个大问题。
返回列表