最近接手了“基于风光储能和需求响应的微电网日前经济调度”这个题目乍看像是论文里常见的模型定义真动手用 Python 去写的时候才发现它要落到程序上解决的事情非常具体未来 24 小时风机该发多少、光伏该发多少、储能是充电还是放电、要不要切掉一部分可中断负荷、从电网买多少电甚至是不是反向卖给电网这些决策全部绑在一起要让微电网在满足负荷的前提下运行成本最低。我花了一段时间把这个模型从零完整实现了一遍包括不确定性场景生成、约束搭建、求解和结果可视化这篇就把整个建模思路、代码结构、参数标定和实际踩过的坑全部整理出来给正在做微电网经济调度研究的同学和搞能源系统开发的工程师一个可以直接对照的参考。1. 先想明白日前经济调度到底在调度什么1.1 目标函数里到底放哪些成本项任何优化问题都要先回答一个问题你优化的目标是什么。微电网日前调度的目标很直接——让第二天的总运行成本最小。但“总运行成本”这个词落到数学上就得拆开看我通常把它写成下面这几个部分的和。与电网交互的成本从配网购电要付电费向配网售电可以回收电费这个成本是分时电价乘以对应功率的累加。常规机组发电成本如果微电网里有柴油发电机或者燃气轮机它的燃料成本一般可以用二次函数近似在线性化时再分段线性化处理。储能循环损耗成本储能每次充放电都会折损寿命把这部分损耗折算成单位充放电成本放进目标函数否则求解器会往死里用储能。需求响应补偿成本让用户削减负荷或转移负荷不是免费的要给补偿这部分也是成本。弃风弃光惩罚成本风光明明发得出来却被迫限制出力通常要加一个惩罚项来避免求解器无缘无故弃掉新能源。很多初学者在写目标函数的时候只放电网购电成本结果算出来的方案非常激进储能全天高频充放、需求响应一刀切、风光全部消纳看起来成本低得离谱实际工程里根本执行不了。把上面这些成本项都放进去之后优化结果才会收敛到一个物理上说得通、经济上又合理的方案。1.2 为什么必须做“日前”而不是只靠实时调度有同行问过我既然实时调度也能根据当前状态做决策为什么还要专门做一个日前调度我的理解是日前调度解决的是“全局规划”问题实时调度解决的是“局部修正”问题。储能就是个最典型的例子。如果只做实时决策储能控制器只知道当前这一刻的电价和负荷它不会主动在凌晨两点电价最低的时候把电充满因为从单时刻视角看充电只意味着花钱。但日前调度能看到完整的一整天价格曲线它会把夜间低谷时的便宜电存下来留到白天晚高峰再用。这就像一个出门旅游的人出发前先把每天的路线定好到了景点之后再根据实时人流做局部调整。没有日前计划做基准实时控制就成了走一步看一步经济性会差很多。所以项目里定的逻辑是先用预测的风光出力、负荷曲线和分时电价做一次日前优化得到全天 24 小时的运行计划实际执行时微电网的能量管理系统再根据实时数据对计划做滚动修正。日前调度的价值就是提供一个全局最优的运行基准。1.3 约束条件一个都不能少目标函数决定优化方向约束条件决定可行域。写微电网模型的时候我把约束条件分成几类每一类漏掉都会让结果失真。功率平衡约束是第一个要写的它要求每个时刻系统发电和用电相等表达式是风机出力加光伏出力加储能放电功率加购电功率加需求响应削减量等于负荷加储能充电功率加售电功率。这个约束如果不满足模型直接不可行。储能自身的约束是第二个重点包括 SOC 递推方程、SOC 上下限、充放电功率上下限。注意 SOC 递推方程里效率的处理充电时 SOC 增加的量要乘以充电效率放电时 SOC 减少的量要除以放电效率两个效率通常不同不能简单用一个数字代替。我还要特别提醒一个经验很多初次写代码的人会把第 0 小时 SOC 初值设了之后忘了约束第 24 小时的 SOC结果优化出来的结果往往是最后两个小时储能把电全部放空目标函数成本大幅下降但这在工程上是不可接受的因为储能每天要留一个合理的端到端电量。我一般会用 SOC(0) SOC(24) 或者允许末端在一个小范围内漂移并给末端偏移加一个惩罚系数。最后是需求响应约束削减负荷不能超过协议规定的上限也不能无限制转移。约束条件不是写得越多越好但上面这几条是骨干缺一条模型就跑偏。2. 风光、储能、需求响应怎么建模才靠谱2.1 风光出力场景怎么从预测曲线生成风电和光伏的出力有天然的不确定性预测数据给的是一个期望值实际可能高可能低。如果直接用一条确定性曲线做日前优化结果对预测误差没有任何免疫力实际执行起来容易偏差。我在这套代码里采用的是场景法思路分三步走。第一步把预测出力曲线当作均值用误差分布模型生成大量随机场景。风电预测误差一般可以近似成正态分布或 beta 分布光伏可以按晴空指数建模。第二步用蒙特卡洛抽样生成几百个场景再通过场景缩减技术把场景数量降到几个或十几个同时保留每个场景的概率权重。第三步把优化目标从“最小化单场景成本”改成“最小化所有场景的期望成本”。用代码描述场景生成的时候看起来并不复杂先定义预测曲线数组再循环生成误差矩阵叠加到预测曲线上得到 N 个场景。真正容易翻车的是场景缩减那一步很多初学者不知道怎么处理权重。我当时用的是同步回代消除的思路每次删除一个与其他场景距离最近的场景把它本身和被删场景的概率加起来给到离它最近的那个场景。这么做之后典型场景就能代表原始场景集的总体分布。如果只是做一个课程设计或者入门 Demo直接用一条典型日的预测曲线也能跑通但论文或项目报告里要写清楚你用的是确定性模型还是随机模型。风光占比越高的微电网不确定性处理越是刚需否则日前计划完全没法看。2.2 储能模型要注意效率和同时充放电储能是微电网经济调度里最活跃的决策变量它的运行模型写得好不好直接决定结果可信度。SOC 递推方程是核心用离散形式表达就是后一时段的 SOC 等于前一时段 SOC加上充电功率乘以充电效率减去放电功率除以放电效率。这套代码里我把能量单位统一成 kWh功率单位统一成 kW时间间隔取 1 小时那么充电功率在数值上等于充电电量。如果时间粒度变成 15 分钟就必须乘以 0.25这一步很多人会忘导致储能电量算出来虚高。充放电不能同时进行这个约束有两种实现方案。严格做法是引入充电状态和放电状态两个二进制变量强行控制同一个时刻只能有一个大于 0。偷懒做法是不加二进制变量在目标函数里加一个小惩罚项比如给充电功率和放电功率的乘积加一个极小的正惩罚使它趋于零。我强烈建议在正式项目里用严格做法因为惩罚项系数很难调到完全不干扰原目标函数而且如果被惩罚项压制的量本身和储能退化成本在同一数量级结果就会变得很怪。二进制变量会让模型从线性规划变成混合整数线性规划求解规模会变大但 24 小时单储能的问题通常只有几十个整数变量绝大多数求解器几秒到几十秒就能算出结果没必要为了省这点计算时间去牺牲模型严谨性。2.3 需求响应的三个层次选哪种看你手里的数据需求响应的建模方式直接决定优化变量里有没有“负的负荷”。我在这套代码里实现了两个层次可以单独用也可以组合。第一层是可削减负荷这是最简单也最常用的模型。把每个时段允许削减的负荷上限设一个比例比如总负荷的 5% 或 8%削减量作为连续优化变量目标函数里加一个补偿单价乘以削减量的成本项。如果削减量上限定的太小整个需求响应对优化结果几乎没有影响如果定得太大超过 20%优化结果就会严重依赖这个措施实际场景里用户根本削减不了那么多。我一般建议在 5% 到 10% 之间具体取决于微电网的负荷类型。第二层是可转移负荷。它比可削减负荷复杂因为转移负荷需要保证总用电量不变只是用电时段发生变化。建模时需要定义负荷的开始时间窗口和持续时间用辅助变量描述“在某时段开始用电”的决策然后通过时间累计关系把转移后的功率表示出来。这种模型在含电动汽车有序充电的微电网里很常见因为电动车充电并不会取消只是挪到另一个时间段。还有一类价格弹性矩阵模型它不需要显式定义削减量而是通过用户对电价的响应系数矩阵把基准负荷曲线映射成响应后的负荷曲线。这种模型适合做电价优化但在日前调度的框架里耦合度比较高容易非线性化我没把它放进主代码只在扩展方案里提了一句。需求响应模型选哪种取决于你手里有哪种用户数据。如果只有比例协议就用可削减如果有具体的可延迟设备用电计划就用可转移。两种模型单独实现都不算难组合起来要特别注意不能让同一个负荷既被削减又被转移否则会造成重复补偿。3. Python 代码实现的关键环节3.1 程序模块怎么划分这套代码我按功能分成五个模块模块之间只通过数据接口打交道这样做的好处是调试的时候能快速定位问题出在哪一层。第一个模块是数据输入模块负责读入负荷预测、风电预测、光伏预测、分时电价、储能参数、需求响应参数。我建议把参数集中放在一个配置文件里不要散落在代码各处不然改一个电价数值要翻遍整个程序。第二个模块是场景处理模块负责把预测曲线扩展成多个随机场景以及做场景缩减。这是整条链路上计算压力相对大的部分但逻辑独立单独测试很方便。第三个模块是模型构建模块定义优化变量、约束条件和目标函数。这是整个项目的核心变量命名要可读比如用 P_wind_t 表示 t 时段风机出力用 SOC_t 表示 t 时段荷电状态一眼能看懂含义。第四个模块是求解模块封装对线性规划求解器的调用统一处理求解状态判断、结果提取和异常捕捉。第五个模块是结果分析模块负责画功率平衡曲线、SOC 曲线和各成本占比图。主脚本只做一件事依次调用上述五个模块并打印关键过程信息。我接手过一些别人的代码最痛苦的就是所有逻辑堆在一个几百行的脚本里变量名用 a、b、c改一个参数像拆炸弹。模块化看起来多写几个文件实际调试的时候能省下大量时间。3.2 模型构建的核心代码骨架优化模型构建部分的代码结构我用一套抽象的建模接口来描述核心骨架实际使用的时候把它映射到你环境里已有的建模工具上就行无论开源的还是商业的接口思路都一样。model 创建优化模型() # 决策变量 P_wind [model.add_variable(lb0, ub300, namefP_wind_{t}) for t in range(24)] P_pv [model.add_variable(lb0, ub200, namefP_pv_{t}) for t in range(24)] P_ch [model.add_variable(lb0, ub100, namefP_ch_{t}) for t in range(24)] P_dis [model.add_variable(lb0, ub100, namefP_dis_{t}) for t in range(24)] SOC [model.add_variable(lb0.1, ub0.9, namefSOC_{t}) for t in range(24)] P_buy [model.add_variable(lb0, ub200, namefP_buy_{t}) for t in range(24)] P_sell [model.add_variable(lb0, ub200, namefP_sell_{t}) for t in range(24)] DR [model.add_variable(lb0, ub100, namefDR_{t}) for t in range(24)] u_ch [model.add_binary_variable(namefu_ch_{t}) for t in range(24)] u_dis [model.add_binary_variable(namefu_dis_{t}) for t in range(24)] # 功率平衡约束 for t in range(24): model.add_constraint( P_wind[t] P_pv[t] P_dis[t] P_buy[t] DR[t] load_seq[t] P_ch[t] P_sell[t], namefbalance_{t} ) # 储能SOC递推约束 for t in range(1, 24): model.add_constraint( SOC[t] SOC[t-1] eta_ch * P_ch[t] - P_dis[t] / eta_dis, namefsoc_update_{t} ) model.add_constraint(SOC[0] SOC_init, namesoc_init) model.add_constraint(SOC[23] SOC[0], namesoc_end_coupling) # 充放电同时性约束 for t in range(24): model.add_constraint(P_ch[t] 100 * u_ch[t]) model.add_constraint(P_dis[t] 100 * u_dis[t]) model.add_constraint(u_ch[t] u_dis[t] 1) # 需求响应上限约束 for t in range(24): model.add_constraint(DR[t] dr_ratio * load_seq[t]) # 目标函数总成本最小 total_cost sum( price_buy[t] * P_buy[t] - price_sell[t] * P_sell[t] c_bat * (P_ch[t] P_dis[t]) c_dr * DR[t] c_curtail * (W_ava[t] - P_wind[t] PV_ava[t] - P_pv[t]) for t in range(24) ) model.set_objective(min, total_cost) # 求解 status model.solve()我写这段骨架代码的时候故意没有绑定任何具体库因为建模接口在主流工具之间高度相似创建变量、添加约束、设置目标、求解。你在自己的环境里把这几行接口替换成实际库的写法就好了。值得多提一句的是弃风弃光惩罚项里 W_ava[t] 表示 t 时段可用风电功率P_wind[t] 是实际消纳的风电功率两者之差就是弃风量。3.3 参数怎么定才合理参数标定是整个项目里最容易被忽略但最能体现工程经验的部分。我把本项目用到的关键参数列成一张表这些数值不一定适配所有微电网但它们之间的量级关系和设置逻辑是通用的。参数数值说明风机额定功率300 kW作为风电出力上限光伏额定功率200 kW作为光伏出力上限储能额定容量500 kWh对应 5 小时持续放电能力储能最大充/放功率100 kW受 PCS 变流器限制储能初始 SOC0.5运行日开始时电量状态SOC 上下限0.1 / 0.9防止过充过放充/放电效率0.95 / 0.95锂电池典型参数购电功率上限200 kW与配电网的变压器的容量售电功率上限200 kW反向送电的线路能力需求响应削减比例8%对负荷峰值段的削减能力需求响应补偿单价1.2 元/kWh略高于平段电价的激励储能退化成本0.2 元/kWh折算循环寿命损耗弃风弃光惩罚系数8 元/kWh必须高于单位购电成本分时电价是另一个非常关键的参数我设了三段谷时 22 点到次日 8 点电价 0.3 元/kWh平时段 8 点到 11 点及 18 点到 22 点电价 0.8 元/kWh峰时段 11 点到 18 点电价 1.2 元/kWh。峰谷价差超过 0.9 元/kWh这样储能才有套利空间。储能退化成本这个数很多人不知道怎么定我提供一个简单折算方法假设储能全生命周期可循环 5000 次总放电量除以储能系统采购成本得到每 kWh 的损耗成本。500 kWh 系统按一个典型投资水平算折算下来每 kWh 大概在 0.1 到 0.3 元之间。你把峰谷价差 0.9 元放进去储能每充放一度电可以赚 0.7 元左右扣除退化成本 0.2 元还有利润所以优化结果自然会优先让储能参与套利。如果你把退化成本设到 1 元以上储能就会选择彻底躺平这是很多项目储能利用率低的一个重要原因。3.4 结果到底怎么看先画三条曲线求解完成之后我通常先画三张图判断结果有没有物理意义。第一张是全天功率平衡图把风机、光伏、储能、购电、需求响应几条功率曲线画在一起直观看到每一小时供给来自哪里负荷缺口是怎么被补上的。第二张是储能 SOC 曲线。正常情况下它应该是一天一个完整循环谷时段 SOC 爬升、峰时段 SOC 下降。如果 SOC 曲线上上下下像锯齿一样频繁震荡多半是模型里少了充放电同时性约束或者储能退化成本太低求解器在制造无意义的频繁充放。第三张是成本构成图把购电成本、售电收益、储能损耗、需求响应补偿分类汇总。看结果时我会重点检查几点需求响应是否只在电价高的时段出现储能是否大部分时间在谷充峰放弃风弃光是否集中出现在风电大发而负荷很低的时段。这些判断标准我总结成一句话日前调度结果的质量不取决于求解器报出的目标函数值有多低而取决于曲线是否符合储能运行常识和负荷供需逻辑。目标函数值再漂亮物理上说不通就是错的。4. 实操中最容易踩的坑与排查方法4.1 模型一跑就不可行先查这两件事模型不可行是刚写完代码最常见的报错。我第一次跑通这套模型的时候花了大半天排查一个奇怪约束后来发现是单位写错了功率用 kW电量用 kWh两个一混约束条件里数值量级差了 24 倍直接导致不可行。遇到不可行先不要慌按照这个顺序排查。第一步看功率平衡约束左右两边的单位是否一致。第二步把所有约束单独注释掉只保留平衡约束跑一次看是否可行然后逐个恢复约束找到是哪条约束把可行域挤压空的。第三步检查 SOC 的初值和终值约束。如果 SOC(0) 和 SOC(24) 都固定成同一个值而储能容量又太小夜间充电能力不足末端可能就卡死。还有一个隐蔽问题在需求响应上限。如果负荷曲线在某个时段掉得很低需求响应上限按百分比计算之后可能变成非常小的量配合功率平衡约束会让该时段无解。我建议把需求响应上限写成 min(dr_ratio * load, dr_max)加一个绝对值兜底。4.2 储能“躺平”完全不动问题不在求解器储能不动作绝对是经济调度模型里最常出现的问题但根源往往不在约束而在目标函数。你做一次敏感性分析就会发现峰谷价差减去储能退化成本如果是负的储能的最佳策略就是不动。这时候哪怕你强制储能每天充放一遍目标函数值也会变得更差。另一种更隐蔽的情况是 SOC 终值约束太紧。如果你把 SOC(24) 固定为 0.8而初始 SOC 是 0.5储能就得想着怎么在最后时段把 SOC 顶回 0.8可能整个后半段都在憋着充电不能正常参与晚高峰放电。我的处理方案是日常运行场景让 SOC(24) 等于 SOC(0)但允许一个 5% 的松弛范围并对越界量加一个适中的惩罚。这样储能既有循环空间又不会在最后时段做极端动作。再有就是检查需求响应补偿是不是设得太低。如果补偿单价远低于峰谷套利收益优化只会优先用储能而不用需求响应这未必是错误但如果你希望看到多措施协同就要调整补偿单价。4.3 结果里同时出现充电和放电必须立刻修如果你在结果表里看到某个时段充电功率和放电功率同时大于 0这个模型一定有缺陷。原因有两个可能一是你没有加充放电状态二进制变量线性规划求解器为了让等式平衡会人为制造同时充放这种没有物理意义的解二是你加了惩罚项但惩罚系数太小惩罚带来的成本增量被其他成本项的下降抵消了。修复方案比较直接就是我在 3.2 节展示的那种做法引入充电状态和放电状态两个二进制变量加一条 u_ch u_dis 1 的约束。修完之后你会发现结果曲线干净很多SOC 曲线也变成了平滑的单峰。虽然变量多了一倍但对 24 时段单一储能系统来说多出来的整数变量完全在求解器处理能力之内。4.4 索引和边界条件这些细节最能看出代码成熟度写调度程序最容易犯的索引错误就是 Python 从 0 开始计数而很多实验数据是从第 1 小时开始编号。你读数据时如果不显式对齐功率平衡约束里所有的 t 都会偏一位最后结果看起来只差了一个小时但全网对不上。SOC 递推里的边界条件也是重灾区。我见过一些代码写 SOC[t] SOC[t-1] ...但是 t 从 1 到 23第 0 时段的值直接是初值。这样做本身没错但很多人漏了第 23 时段到第 0 时段的循环衔接等于储能每天运行结束时的电量是“凭空消失”的。正确做法是明确定义 SOC[0] 为初始状态再写 SOC[23] 与 SOC[0] 的耦合约束同时需要考虑耦合后第 23 时段的 SOC 变化对自身约束的正常约束关系。另外如果数据文件里时间跨度跨天比如凌晨 0 点到第二天凌晨 0 点建议统一使用 0 到 23 的索引不要用 1 到 24。这个小习惯能避免很多画图和数据对齐的问题。4.5 计算时间变长以后从这三处优化如果模型只是 24 小时单场景求解时间基本可以忽略。但当你加入 20 个随机场景之后决策变量和约束数量会成倍增长求解时间可能从几秒涨到几分钟甚至更久。我的经验是优先做三件事。第一考虑把场景数从 20 个减到 10 个通过场景缩减技术挑选最具代表性的场景损失一点随机性但计算速度能提升一倍以上。第二约束条件里如果有大 M 常数检查 M 的取值是否紧凑。比如充放电约束里用的 100 是最大功率这已经是紧凑取值如果你随手写一个 10000会严重劣化求解器的数值稳定性。第三给求解器设置一个可接受的相对误差或时间上限比如允许 1% 的次优性来换取更快的求解速度。在工程场景下1% 的目标函数误差完全不影响决策质量。5. 这个项目还能怎么升级5.1 从单场景确定性模型走向随机优化我前面主要讲的是确定性模型加场景生成但严格意义上的随机优化是让模型在优化时同时考虑多个场景目标函数变成各场景成本的期望值。实现上只要把决策变量分成第一阶段变量和第二阶段变量第一阶段是独立于场景的决策比如机组启停状态、储能每天的循环计划第二阶段是依赖于场景的调整量比如各场景下的实际出力。这个升级是微电网调度研究的常见方向代码框架不需要推翻重来只是在现有变量上增加场景维度。5.2 引入多类型储能与碳交易单一电池储能只能做短时能量搬移如果把氢储能或压缩空气加进来模型能处理更长时间尺度的能量调度。比如电池负责日内峰谷套利氢气储能负责跨日甚至跨周的调节。与此同时如果项目涉及碳交易背景可以在目标函数里加一个碳排放成本项给燃煤机组功率乘以碳强度系数再乘以碳价各类能源的发电优先级会随之改变。5.3 用滚动时域控制替代一次性求解日前调度一次性规划 24 小时最大的问题是预测偏差没有得到及时纠正。一个实用方案是把模型改进成模型预测控制的形式每 1 小时或者每 15 分钟滚动求解一次未来 24 小时的调度计划但只执行当前时刻的决策下一时刻重新求解。这样既保留日前全局优化的视野又能在执行层面对抗不确定性。代码改造上相当于在主循环外面再套了一个时间推进循环。5.4 多微电网协同调度最后一种扩展是把单个微电网升级成微电网群各微电网之间可以互相购售电功率交互成为额外决策变量。这种模型对联络线功率约束和跨网交易定价机制的要求比较高但是非常契合目前园区级综合能源系统的发展趋势。如果是从这套代码起步可以先在目标函数里增加一个微网间购售电的分项再把每个微电网内部的功率平衡约束拆开难度升级就落在多微网电量电价耦合上面。最后再分享一点个人体会这套代码完整跑通之后我最大的体会是经济调度模型的核心不在于用了多复杂的求解算法而在于你对物理系统里每个环节运行成本的理解。储能退化成本、需求响应补偿单价、弃风弃光惩罚系数这三个数标定错了再高级的算法也算不出可执行的方案。我自己一开始把储能退化成本设得偏高结果整套优化出来的储能动作是零当时第一反应怀疑代码写错了排查了一整天才发现是参数问题。所以建议刚上手的朋友拿到任何一套调度代码先做一次参数敏感性分析把每个关键成本都调大一倍和调小一倍各跑一遍观察系统行为变化方向是否符合常识这一步做完再讨论算法效果。另外模型别一开始就追求高复杂度先把确定性模型跑出合理结果再逐步加随机场景和滚动优化每一步都能停在可解释、可验证的状态这才是真正能落地的项目节奏。