
搞电网调度优化这些年我越来越确认一件事论文里写得漂亮的模型落成代码时总会在几个地方卡住。储能电站接入后的源储荷协调调度算是这几年最热的方向之一而“考虑特性分布”这个定语看着偏学术背后其实是一个特别实在的工程问题——储能电站里几千节电池状态本来就不一样你把它当成一个“大电池”去调度计划做得再完美执行层也会打折。这个项目就是一套完整的Matlab实现把储能电站按电池特性分成若干簇在日前、日内、实时三个时间尺度上滚动协调机组、储能和负荷最终输出可直接下发的调度指令。文章会从模型怎么建、代码怎么组织一直聊到调试过程中踩过的坑。适合正在做储能并网、微电网优化或者电力系统课题的研究生和工程师参考。已经熟悉优化调度基础的朋友可以直接跳到第3章看模型细节代码相关的内容在第4章。1. 项目背景储能“特性分布”和“多时间尺度”到底在解决什么问题1.1 特性分布不是统计名词是运行痛点储能电站从外面看是一个整体里面是成千上万节电芯通过串联并联组成的电池系统。新投运的时候一致性还行跑个一年半载因为电芯本身的制造公差、柜内温度场的差异、充放电深度的不同单体之间的SOC、SOH、内阻、可用容量就开始明显分化。这个分化的统计规律就是标题里说的“特性分布”。如果把电站当成一个等效大电池来调度相当于默认所有电芯状态都一样。但实际上不是。我做过一个100MW/200MWh的调频储能项目投运两年后实测站内SOH最高的簇还有96%最低的已经掉到82%。用聚合模型排计划按整站额定容量去充放电那些低SOH的簇就会被反复推到SOC上限BMS频繁告警甚至直接降功率结果整站实际出力曲线跟日前计划对不上考核损失很大。所以“考虑特性分布”本质上就是把一个运行层面的问题拉到调度决策层面不是事后去均衡电池而是在排计划的时候就把“哪些簇能干、哪些簇要省着用”这件事算进去。这也是为什么项目里我没有采用传统的单储能聚合模型而是做分簇建模让优化器自动感知每簇电池的差异。1.2 为什么非要“多时间尺度”不可风电光伏的波动跨越多个时间尺度秒级的瞬时脉动、分钟级的云层遮挡、小时级的天气过程变化。负荷也一样既有日内峰谷的大趋势又有随时出现的短时扰动。火电机组能扛大功率变化但爬坡慢储能响应快但容量有限。单一时间尺度上同时解决“大计划”和“微修正”要么模型大到解不动要么精度根本不够。多时间尺度的思路是分层治理。日前层用1小时分辨率做24小时经济调度定机组启停和储能基准曲线日内层用15分钟分辨率、4小时滚动窗口修正新能源和负荷预测误差实时层用1到5分钟的周期平抑剩余波动。每一层只解决本层该解决的问题把预测精度和求解速度匹配起来。这个思想跟自动控制里的串级控制很像内环快、外环慢各司其职。项目里三层模型并不是三个独立问题而是层层嵌套、基准加修正的关系这个衔接逻辑是整套代码的灵魂。2. 整体设计思路与方案取舍2.1 源储荷统一调度的价值传统调度是“源随荷动”储能往往被单独调度或者干脆不参与。但这个项目把源、储、荷放进同一个优化问题里协调原因是三者在时间特性上高度互补储能削峰填谷、机组跟进大趋势、负荷侧通过需求响应做柔性调整。统一建模之后储能的调峰、调频、备用价值可以在一个目标函数里被公平权衡而不是人为地先定储能曲线再让机组去追那样很容易造成储能空转或者机组反复调节。我在代码里把风电场、常规机组、储能电站、可调负荷统一封装成资源池每个资源都有功率上下限、爬坡速率和成本函数。调度模型不关心你具体是哪种设备只关心“下一时刻谁出力最便宜、谁响应最快”这在实际工程里非常有用因为不同运行方式下资源的角色是动态变化的。2.2 特性分布建模分群聚合而不是逐节建模理论上最精确的做法是把每一节电池都建进模型但一个中型储能站上万节电芯优化变量直接爆炸任何商用求解器都扛不住。工程上可行的方案是“分群聚合”先用聚类算法把单体按特性分成若干个簇每个簇再等效成一个聚合储能单元。我在代码里用的是K-means特征取SOH、内阻、最大允许倍率和当前SOC四个量聚成3到5簇。太少丢信息太多求解慢3到5是个经验上比较舒服的区间。分簇之后每个簇有自己的可用容量、充放电效率、功率上限和SOC运行区间。比如SOH低的簇SOC允许区间收窄到0.2到0.8SOH高的簇放宽到0.1到0.9。这样即便在优化模型内部也能体现“健康电池多出力、老化电池少出力”的实际运行规则。后面会讲到这些参数就是直接通过BMS数据或者模拟数据经过聚类生成的整个流程在代码里是一条线从原始单体数据到优化参数一气呵成。2.3 工具链选型为什么是Matlab YALMIP GurobiMatlab做这个事有两个不可替代的优势一是矩阵化表达和调度问题的天然契合二是YALMIP这类建模工具把优化模型写得和数学表达式几乎一一对应排错直观。求解器方面我首选Gurobi没有的话CPLEX也行再不行就退回intlinprog。虽然intlinprog免安装但混合整数规划规模一大速度和Gurobi的差距是数量级的尤其是带有几百个二进制变量的问题这个差距会非常明显。YALMIP需要自己下载添加到路径Gurobi需要License配置过程不算复杂但建议一开始就按“优化器参数可配置”的方式写在代码里不要硬编码在模型文件中。这个项目里主要求解的是MILP所以建模时会特别注意把非线性项线性化能用线性绝不用非线性这是MILP建模的黄金法则。2.4 为什么不直接上强化学习这两年DQN、PPO这些强化学习算法很火也有人问我为什么不直接用强化学习做调度。我的看法是对于日前和日内这种“约束条件极多、安全要求极高”的决策问题数学规划的可解释性和约束满足保证是RL短时间替代不了的。RL更适合实时层那些环境不确定、动作空间小、可以试错的场景。如果非要用RL我建议也是先拿数学规划的结果做专家轨迹再去做模仿学习或者离线RL而不是一上来就端到端。这个项目里我全部走数学规划路线稳定可控每一层的每个约束都有明确的物理含义调度员拿到结果敢用。3. 数学模型拆解从目标函数到约束全景3.1 目标函数成本、惩罚与储能的“机会成本”日前调度层的目标函数是系统总运行成本最小我把它写成四项相加$$\min \sum_{t1}^{T} \left[ \sum_{g1}^{N_G} \left( a_g P_{g,t}^2 b_g P_{g,t} c_g \right) \sum_{k1}^{K} c_k^{ess} \left( P_{k,t}^{ch} P_{k,t}^{dis} \right) \lambda_{wind} P_t^{curt} \lambda_{load} P_t^{shed} \right]$$第一项是常规机组燃料成本a、b、c是耗量特性系数P是机组出力第二项是储能充放电的寿命损耗成本按各簇等效循环老化折算成每兆瓦时成本第三项是弃风惩罚第四项是切负荷惩罚。后面两个惩罚项的系数要远大于燃料成本才能保证系统优先保供电、其次消纳新能源最后才考虑经济性。这个优先级排序是调度规则的核心系数一旦设反优化器会为了省钱故意切负荷那是绝对不能接受的。这里有个容易被忽略的细节储能的成本项我没有简单用“充放电量乘固定系数”而是按簇区分——SOH越低的簇单位充放电的寿命损耗成本越高。这样优化器会自发地优先调用健康簇让老化簇少动作等效于在目标函数层实现了“特性分布感知”。实际运行中这种做法的直接收益就是全站电池的一致性恶化的速度明显放缓。3.2 约束条件功率平衡、机组、储能与特性分布约束约束分四组。第一组是功率平衡任意时刻所有机组出力加储能净放电加风电消纳量等于负荷这是硬约束不能有偏差。第二组是机组约束包括出力上下限、爬坡速率限制和最小启停时间。启停状态用0-1变量表示出力上下限和启停状态相乘会引入非线性统一用big-M法线性化把乘积项拆成三条线性不等式交给MILP求解器处理。第三组是储能约束对每个簇单独建包括充放电功率上下限、SOC动态方程、SOC运行区间以及“同一时刻不能同时充放电”的互斥约束。SOC动态方程是$$SOC_{k,t1} SOC_{k,t} \frac{\eta_k^{ch} P_{k,t}^{ch} \Delta t}{E_k^{max}} - \frac{P_{k,t}^{dis} \Delta t}{\eta_k^{dis} E_k^{max}}$$不同簇的充放电效率、可用容量都不同这本身就是特性分布的一部分。第四组也是这个项目比较特别的地方——特性分布约束。分簇之后各簇的SOC上下限、功率倍率上限、寿命损耗成本都不一样。另外我还会加一个全站总SOC的加权一致性约束要求各簇SOC按容量加权的平均值跟随系统层的调度指令避免簇间出现“有些簇满了、有些簇空了”的失衡状态。这个约束在实际调试中非常有用它保证了分簇模型和聚合模型在宏观层面的一致性让调度结果既尊重单体差异又不会偏离整个电站的物理总量。3.3 三层时间尺度的衔接逻辑三层的优化问题不是三个独立的模型而是层层嵌套的关系。日前层求解得到机组启停状态、备用容量和储能基准SOC曲线这些结果作为边界条件传给日内层。日内层不再动机组启停只调机组出力修正量和储能的充放电计划并加入松弛变量允许对日前计划做有限偏离偏离越远惩罚越大。实时层则把储能作为主要调节手段在日内计划基础上做二次规划目标是最小化跟踪误差和调节代价。衔接的关键是“基准加修正”的思路下层永远不推翻上层的大决策只在上层划定的框架内做局部优化。如果让日内层完全自由优化机组启停就会来回波动火电受不了调度员也不敢用。项目代码里日前层输出的u_g_dayahead会作为日内层的固定参数传入日内层只有连续变量这样既保证了计算效率又维持了三层决策的一致性。4. Matlab代码实现架构与核心片段4.1 工程文件怎么组织代码我按“数据、参数、模型、求解、出图”五个层次组织不把所有东西堆在一个脚本里。主程序main.m按顺序调用各模块算例路径统一配置。数据读取、参数初始化、分群聚类、日前建模、日内滚动、实时二次规划、结果可视化每个功能一个文件公共函数放进utils目录。这个结构的好处是想改算例、换机组参数、加新约束时不会牵一发动全身。我见过很多同学把整个模型写在2000行的一个脚本里改一个参数要找半天跑错一次debug都很痛苦。建议代码里所有关键参数比如时间分辨率、窗口长度、储能分簇数、惩罚系数都集中在init_params.m里用结构体组织。这样后期做敏感性分析时只需要循环改参数不需要动模型代码。我自己的习惯是每个算例跑完自动把结果存成mat文件方便多个算例之间对比这个习惯帮我省了大量重复跑数的时间。4.2 特性分布参数生成与分群聚类如果没有实测BMS数据可以用正态分布生成一批模拟单体参数然后用K-means聚类。核心代码如下% 生成N_cell节电池单体每节四个特性SOH、内阻、最大充电倍率、初始SOC N_cell 5000; SOH 0.82 0.15 * randn(N_cell, 1); R_in 0.5 0.08 * randn(N_cell, 1); C_rate 0.5 0.1 * randn(N_cell, 1); SOC0 0.5 0.15 * randn(N_cell, 1); % 裁剪到合理区间避免出现物理上不可能的参数 SOH min(max(SOH, 0.7), 0.98); R_in min(max(R_in, 0.2), 1.0); C_rate min(max(C_rate, 0.2), 1.0); SOC0 min(max(SOC0, 0.1), 0.9); feat [SOH, R_in, C_rate, SOC0]; feat_norm (feat - mean(feat)) ./ std(feat); K 4; [idx, center] kmeans(feat_norm, K, MaxIter, 500, Replicates, 5);聚类之后对每个簇做聚合参数计算。这里有个容易踩的坑不要直接用单体参数的算术平均作为聚合参数。比如一簇里200节电池聚合放电功率上限应该是各节上限之和聚合内阻近似等于并联支路数的平均SOC初始值是按能量加权的平均。不同参数的物理含义不同聚合算法也必须不同。下面这段代码展示了聚合过程for k 1:K idx_k find(idx k); E_max(k) sum(E_cell(idx_k) .* SOH(idx_k)); Pch_max(k) sum(C_rate(idx_k) .* E_cell(idx_k)); Pdis_max(k) sum(C_rate(idx_k) .* E_cell(idx_k)); eff_ch(k) 0.95 - 0.05 * mean(R_in(idx_k)) / 0.5; SOC_init(k) sum(E_cell(idx_k) .* SOH(idx_k) .* SOC0(idx_k)) / E_max(k); soc_ub(k) 0.9 - 0.1 * (1 - mean(SOH(idx_k)) / 0.9); soc_lb(k) 0.1; c_deg(k) 50 80 * (0.95 - mean(SOH(idx_k))); endSOH低的簇E_max按比例折算SOC上限收窄退化成本提高。这三个参数同时作用优化结果自然会让健康簇承担更多调节任务。4.3 日前调度建模核心YALMIP建模的关键是把决策变量定义清楚。机组有功、启停状态、储能充放电功率、SOC、风电消纳量各自是什么维度、什么类型一开始就要想清楚否则后面约束拼接时维度对不上会非常痛苦。下面这个片段定义了核心变量和功率平衡约束T 24; NG 6; K 4; P_g sdpvar(NG, T, full); u_g binvar(NG, T, full); Pch sdpvar(K, T, full); Pdis sdpvar(K, T, full); SOC sdpvar(K, T1, full); Pwind sdpvar(1, T, full); Pcurt sdpvar(1, T, full); Constraints []; % 功率平衡约束 for t 1:T Constraints [Constraints, ... sum(P_g(:,t)) - sum(Pch(:,t)) sum(Pdis(:,t)) Pwind(:,t) Load(t)]; end % 机组出力上下限big-M形式0 P Pmax * u for t 1:T for g 1:NG Constraints [Constraints, 0 P_g(g,t) Pmax(g) * u_g(g,t)]; % 爬坡约束、最小启停时间约束略 end end目标函数里的平方项a乘以P的平方MILP不能直接处理我用分段线性化逼近把0到Pmax的功率区间切成4到6段每段引入连续变量用凸组合表示。这样精度够用求解速度却快很多而且Gurobi对线性约束的处理远比对二次项激进得多。这个替换在工程上是完全值得的因为机组耗量特性本身也是拟合出来的分段线性并不会比二次多项式损失多少精度。4.4 日内滚动优化主循环滚动优化的框架很像模型预测控制每个周期用最新预测刷新窗口内的模型只下发第一个时段的指令然后窗口向前滚动。代码结构如下N_window 16; t_now 1; while t_now T_intraday - N_window 1 idx_win t_now : t_now N_window - 1; Load_win load_pred(:, idx_win); Wind_win wind_pred(:, idx_win); % 构建窗口内优化模型固定日前启停状态 % 出力修正量delta_pg为连续变量加二次惩罚项 % 储能SOC初始值用上一窗口末值 % 求解后只下发第一个时段的P_g和P_ess Dispatch_Pg(t_now) value(P_g_opt(:, 1)); Dispatch_Pess(t_now) value(Pdis_opt(:,1) - Pch_opt(:,1)); t_now t_now 1; end这里最常见的错误是窗口滑动步长和下发指令数量不匹配。有人写循环时让窗口每次前进N_window个时段那中间时段的指令就丢了调度曲线会出现断档。正确做法是每次都前进一个时段同时每个窗口都重新优化虽然计算量上去了但这才符合滚动修正的意义。为了缓解计算压力可以用YALMIP的optimizer对象把模型编译一次之后每次只更新参数重新求解而不是反复重建模型实测能快一半以上。5. 调试心得与常见问题速查5.1 求解慢先查变量规模、MIP gap和建模方式如果Gurobi跑一个日前调度都要几十秒甚至更久我一般按三个方向排查。一是看变量规模尤其是二进制变量数量。机组启停变量NG乘T乘层数是最主要的来源如果每层都重新定义二进制变量规模立刻膨胀。解决方法是日内和实时层固定启停状态不做0-1优化。二是看求解器参数把MIP gap从默认的1e-4放宽到5e-3对工程场景精度完全够速度提升往往非常可观options sdpsettings(solver, gurobi, gurobi.MIPGap, 0.005, verbose, 0);三是看是否把不必要的约束写成了非线性。分段线性化优于二次项big-M优于乘积项能用线性绝不用非线性。这三个方向排查完之后绝大多数性能问题都能解决。5.2 SOC漂移与数值病态SOC更新是差分方程如果用显式欧拉离散长时间多窗口滚动之后SOC会出现累积漂移也就是窗口衔接处SOC不连续、越滚越偏。我的解决办法是每次窗口优化结束后把SOC的末值显式取出来作为下一个窗口的初始值传入并且单独加一条“窗口末SOC必须等于外部传入初值加本窗口充放电累计”的耦合约束。同时把时间步长从小时统一换算成与功率、容量量纲一致的时间避免单位不一致导致数值病态。另外目标函数里不同量的量级差别很大——机组燃料成本是万元级惩罚项可能设到百万级储能成本是百元级。如果不做无量纲化数值求解会很不稳定。我习惯把所有成本项统一除以一个基准值比如10000元让各项系数落在1到100之间求解器收敛速度会明显提升。这个技巧虽然不起眼但很多时候“莫名其妙不收敛”的问题就是出在这里。5.3 特性分布参数从哪来没有实测数据怎么办这是被问得最多的一个问题。有BMS或SCADA历史数据当然最好直接从电站监控系统导出各簇电池的充放电曲线、温度、电流、电压用SOC-OCV曲线拟合和内阻辨识方法提取参数。但很多同学做课题时没有真实电站数据我的建议是用参数化场景生成按正态分布生成多组特性参数调节标准差来模拟“一致性好的新电站”和“老化严重的旧电站”两种场景对比调度结果。这个对比本身就是论文里很有说服力的算例能清楚地展示特性分布对调度决策的影响。K的选择可以用肘部法则画簇内离差平方和随K变化的曲线找一个拐点即可。5.4 常见问题速查表下面这张表是我在实际调试中反复遇到过的现象和对应的处理办法建议直接收藏现象可能原因排查与解决求解器报infeasible约束自相矛盾常见于功率平衡约束和SOC初值不匹配先注释掉储能约束看是否能求解逐步二分定位冲突源结果里储能基本不动储能成本系数设太高或者SOC区间太窄检查c_deg数量级对比储能单位成本与机组边际成本日内结果与日前差距大窗口内预测偏差大且无惩罚约束给修正量加二次惩罚项让日内尽量贴近日前计划SOC曲线振荡充放电互斥约束缺失或效率参数设置异常检查0-1互斥约束是否加入效率参数是否在0.7到1之间滚动循环总是在最后一个窗口报错窗口索引越界在while循环前打印t_now和边界确认窗口长度与总时段数的关系结果里不同簇储能出力完全一样特性分布参数没生效各簇参数被统一赋值检查分簇聚合循环打印各簇E_max、soc_ub、c_deg是否有差异6. 一点个人体会这个项目做下来我最大的感受是模型复杂度不等于模型价值。很多人一开始就想把特性和多时间尺度做到极致结果模型堆得巨大求解器跑不动最后只能靠删约束交差。其实更有价值的功夫在数据侧和衔接侧——特性分布参数怎么从数据里提取、各层之间怎么传递边界条件这两个地方做好了模型本身用最基础的MILP就能出很好的结果。另外如果有条件建议把储能电站的仿真模型或者至少一个简单的SOC响应模型加上去把调度指令闭环回放到电池层面。你会立刻看到哪些计划在单体层面是执行不了的。调度和运行的闭环验证才是这类课题真正的分水岭。我最后再分享一个小技巧做多时间尺度滚动时把每一层的求解时间和目标函数值都打点记录下来画在一张图里。这些数据既能帮你判断计算瓶颈在哪也是论文里展示算法效率的第一手素材。