
接到这个园区微电网日前经济调度项目时我最初的反应是这题不难把光伏出力、风电出力、负荷预测放到一张表里再按分时电价挑便宜的时段买电配合储能低充高放不就行了。但真正动手用Python建模之后才发现风光储能和需求响应一旦耦合进同一个优化框架问题就完全变了——你没法再用几条if then规则去描述最优策略因为储能的SOC会跨时段传递需求响应又会让负荷曲线跟着电价变化。这篇文章把我从数学建模、求解器选型到代码实现、算例复盘的全过程记录下来希望能给正在做微电网调度、综合能源优化或者想用Python调用求解器解决实际调度问题的朋友一些参考。1. 从光伏优先到全局寻优这个项目的调度问题到底长什么样1.1 日前调度的真正难点在于时间耦合微电网日前经济调度的基本任务是已知未来24小时的光伏出力、风电出力和负荷预测曲线也已知分时电价要在满足所有运行约束的前提下决定每个时段的光伏/风电出力、储能充放电功率、从电网买电/卖电功率以及需求响应负荷的转移方案让全天的运行成本最低。如果只看单个时段问题确实简单光伏不够就买电电价高就少买储能没电就充。但一旦把24小时放在一起看麻烦就来了。储能电池的SOC像一条贯穿全天的时间线凌晨充了多少电决定晚上高峰能放多少电需求响应把晚高峰的负荷挪到中午又会反过来影响光伏的消纳空间和买电曲线。这些决策不是孤立的而是跨时段、跨设备的耦合问题必须放在同一个数学模型里统一求解。我把这个问题拆成三个关键矛盾第一光伏中午大发但负荷不一定大多余电量是弃掉、卖给电网还是充进储能第二晚高峰电价高但风光出力可能下降储能的电量应该留到几点放、放多少第三需求响应负荷可以转移但转移本身有补偿成本还要保证一天之内转移的负荷总量平衡不能把高峰负荷凭空搬没了。这三件事单独拿出来都不复杂放在一起就需要一个全局优化框架来处理。1.2 一个让我决定认真做这个项目的实际场景这个项目来自一个园区型微电网配置大概是光伏300kW、风电150kW、储能200kWh/100kW峰值负荷约350kW。原来运行方式很简单就是光伏优先、不够就买电。某天下午光伏出力因为云层遮挡在半小时内从280kW掉到80kW储能系统因为没有提前预留容量只能眼睁睁看着园区从电网高价购电那一天的电费比平常多出好几百块。当时我就意识到靠人工经验和固定规则做调度根本扛不住风光出力的随机波动和电价波动。正确做法是把明天的天气预测、负荷预测、分时电价全部输入模型让优化算法自己算出明天的储能充放电计划、购售电计划和需求响应方案。这也正是日前经济调度这个词的工程含义提前一天做决策给运行人员一份可执行的24小时机组启停和功率计划。2. 把运行规则翻译成数学语言目标函数与五组约束的工程含义2.1 目标函数每一分钱都对应一个决策变量我采用的模型是混合整数线性规划MILP目标函数如下min Σt [ c_buy(t)·P_buy(t) − c_sell(t)·P_sell(t) c_DR·(δ_up(t) δ_down(t)) c_OM·(P_ch(t) P_dis(t)) ]逐项解释一下。c_buy(t)是分时购电价P_buy(t)是t时段从电网买的功率c_sell(t)是上网售电价P_sell(t)是卖给电网的功率因为售电是收入所以前面用负号。c_DR是需求响应补偿单价δ_up(t)和δ_down(t)分别表示t时段负荷上调量和下调量无论上调还是下调都需要给用户补偿所以两项都计入成本。c_OM是储能运行维护成本单价充放电都会消耗电池寿命按充放电电量计费。目标函数里没有直接写光伏和风电的发电成本因为光伏和风电的边际成本接近零模型会在约束允许范围内尽可能地利用它们。这符合实际工程经验新能源优先消纳只有在发出电力无法被负荷、储能和电网消纳时才考虑弃风弃光。2.2 功率平衡约束全系统的守恒关系功率平衡是调度模型的心脏它是这么写的P_pv(t) P_wt(t) P_dis(t) P_buy(t) δ_down(t) P_load(t) P_ch(t) P_sell(t) δ_up(t)公式左边是电源侧光伏出力、风电出力、储能放电、电网购电再加上需求响应中被下调的那部分负荷——为什么下调负荷要放在左边因为负荷下调相当于系统少消耗了一部分功率等价于多了一部分电源。公式右边是负荷侧原始负荷、储能充电、向电网卖电再加上需求响应中被上调的那部分负荷。这里最容易搞混的就是δ_up和δ_down的方向。我建议写代码前先在纸上画一条母线把每个设备标在母线两侧再写平衡方程否则很容易把符号写反导致调度的结果完全不合理。另外P_pv(t)和P_wt(t)是决策变量而不是固定的预测值它们的上限是预测出力模型可以在必要时主动弃光、弃风这在光伏装机容量远大于本地负荷的园区特别重要。2.3 储能约束SOC更新的线性化表达储能是微电网里最特殊的设备因为它有时间记忆需要一组约束来刻画SOC(t1) SOC(t) (η_ch·P_ch(t) − P_dis(t)/η_dis)·Δt/E其中SOC(t)是t时段开始时的荷电状态η_ch和η_dis分别是充电效率和放电效率E是储能容量Δt是时间间隔。这里有一个非常容易忽略的细节如果时间粒度不是1小时Δt必须写进公式否则SOC的累计电量会差出一个倍数。储能模型的另外两组约束是充放电功率上下限和SOC上下限0 ≤ P_ch(t) ≤ P_rate·u_ch(t)0 ≤ P_dis(t) ≤ P_rate·u_dis(t)u_ch(t) u_dis(t) ≤ 1SOC_min ≤ SOC(t) ≤ SOC_maxu_ch和u_dis是0-1变量用来保证同一时刻储能不能同时充电和放电。SOC上下限一般取0.10.9这样可以延长电池寿命也避免过充过放带来的安全问题。除此之外我还会加一条SOC(T) ≥ SOC(0)要求一天结束时储能电量不低于当天开始时的水平否则调度结果会透支电池电量连续运行几天就会出现无电可放的尴尬局面。2.4 需求响应约束可转移负荷如何建模需求响应在本项目里用的是可转移负荷建模思路是把一部分柔性负荷比如充电桩、温控负荷、可中断的生产线从高峰时段挪到低谷时段。数学上每个时段引入上调量δ_up(t)和下调量δ_down(t)再约束这两者之和在整个调度周期内守恒Σt δ_up(t) Σt δ_down(t)这个守恒约束是需求响应建模的关键。它的物理含义是你今天把晚高峰的100kWh负荷挪到中午中午就必须真的多消耗100kWh负荷总量不能凭空减少。很多初学者容易漏掉这一条结果优化出来的成本特别低因为模型实际上是偷偷扔掉了负荷这在物理上根本不可行。同时还要限制每个时段的转移量上限我一般取该时段原始负荷的15%20%最多不超过一个固定的功率限额。这么做一方面是响应真实的可调节能力另一方面是防止模型过度激进地把所有高峰负荷都搬走导致实际运行中用户舒适度和生产计划完全被破坏。2.5 与电网交互的边界条件微电网并网运行时和电网的交换功率也不是无限制的。首先买入和卖出功率都不能超过联络线容量一般取配变容量的80%左右其次同一时段不能既买电又卖电否则如果售电价高于低谷购电价模型会疯狂低买高卖套利这在实际电力市场里是不允许的。买卖互斥约束我通常用一个额外的0-1变量u_bs来实现P_buy(t) ≤ buy_max·u_bs(t)P_sell(t) ≤ sell_max·(1 − u_bs(t))3. 用Python建模前的三个关键选择求解器、数据粒度和风光处理方式3.1 为什么最终选MILP而不是遗传算法在我最开始设计这个项目时有同事推荐用遗传算法或者粒子群算法理由是这类问题反正有非线性约束智能算法好写。但我最终选择了MILP原因有三个。第一MILP能给出全局最优解而启发式算法只能给出近似解。日前经济调度是一个凸包络结构较强的优化问题虽然因为充放电互斥引入了0-1变量但连续部分都是线性的完全可以用MILP在秒级时间内求出全局最优解。对于只有24个时段的中等规模问题几十个0-1变量的求解量对现代求解器来说几乎没有压力。第二约束修改方便。MILP建模语言我用的CVXPY写出来的约束和数学表达式几乎一一对应后续想加一个联络线容量约束、改一改储能SOC上下限直接改一行就行。而启发式算法一旦要改约束罚函数设计和参数调优都是不小的工程。第三结果可解释、可复现。MILP每次求解出来都是同一个最优值方便和同事、客户讨论也方便做多场景对比。遗传算法每次运行结果可能不一样商业项目里很难向甲方解释为什么这次优化出来和上次不一样。3.2 分时电价与预测曲线等数据准备做调度前最重要的准备工作是把数据整理成统一的时间序列。我习惯把所有数据都转成24个时点、间隔1小时的格式时间轴对齐很重要否则后面建模时索引错位会非常难排查。分时电价我按国内常见的峰平谷三段设置0:00-7:00谷段0.40元/kWh8:00、13:00-16:00、23:00平段0.80元/kWh9:00-12:00、17:00-22:00峰段1.20元/kWh。上网售电价统一按0.35元/kWh处理低于所有购电价避免套利空间。光伏出力预测值我用的是一条典型晴天曲线中午12点左右达到峰值95kW早晚为零。风电出力相对平稳全天在1525kW之间波动。负荷曲线则呈现明显的早高峰和晚高峰特征晚高峰约120kW凌晨低谷约35kW。实际项目中这些数据都应该来自天气预报和历史运行记录我这里用的是经过处理的典型日数据方便复现。储能参数容量200kWh、额定功率50kW充电效率95%放电效率92%SOC范围0.10.9起始SOC设为0.2并要求一天结束时SOC≥0.2。需求响应每时段最大转移量15kW补偿单价0.2元/kWh。4. 代码讲解变量组织、约束装配和求解器调参的完整写法4.1 用CVXPY定义优化问题的整体结构我用的是CVXPY加HiGHS求解器。CVXPY的好处是语法非常接近数学公式写起来快后续维护也容易。代码整体分四步定义参数、创建变量、组装约束和目标函数、求解并提取结果。import cvxpy as cp import numpy as np T 24 dt 1.0 # 参数光伏预测、风电预测、负荷预测、分时购电价、售电价 P_pv_f np.array([0,0,0,0,0,5,15,35,60,80,90,95,85,75,55,35,15,5,0,0,0,0,0,0]) P_wt_f np.array([15,18,20,22,20,18,16,14,16,18,20,22,20,18,16,14,16,18,20,22,20,18,16,15]) P_load np.array([50,45,40,38,35,40,60,90,110,120,115,110,105,100,105,110,115,120,115,100,80,65,55,48]) c_buy np.array([0.40]*8 [0.80,1.20,1.20,1.20,0.80,0.80,0.80,0.80,1.20,1.20,1.20,1.20,1.20,1.20,0.80,0.40]) c_sell np.array([0.35]*T) # 储能参数 E 200.0 P_rate 50.0 eta_ch 0.95 eta_dis 0.92 SOC_min, SOC_max 0.1, 0.9 SOC_init 0.2 # 需求响应参数 DR_limit 15.0 c_DR 0.2这里我需要说明一下购电价数组的写法8个谷段0.408点平段0.809-12峰段1.2013-16平段0.8017-22峰段1.2023点平段0.80最后一个0.40其实对应0点我做了环形处理。实际编码时建议直接把24个值按顺序列全避免索引错乱。4.2 变量定义与约束装配的关键细节决策变量定义如下# 决策变量 P_buy cp.Variable(T, nonnegTrue) P_sell cp.Variable(T, nonnegTrue) P_ch cp.Variable(T, nonnegTrue) P_dis cp.Variable(T, nonnegTrue) u_ch cp.Variable(T, booleanTrue) u_dis cp.Variable(T, booleanTrue) u_bs cp.Variable(T, booleanTrue) SOC cp.Variable(T1, nonnegTrue) delta_up cp.Variable(T, nonnegTrue) delta_down cp.Variable(T, nonnegTrue)SOC定义为T1维表示0到24共25个时段的荷电状态这样初始SOC和最终SOC都有明确的位置写约束时不至于索引越界。约束装配我用列表生成式这样代码简洁且不易漏约束constraints [] # 储能SOC递推 constraints [SOC[0] SOC_init] constraints [SOC[t1] SOC[t] (eta_ch * P_ch[t] - P_dis[t] / eta_dis) * dt / E for t in range(T)] # 储能充放电互斥与功率上下限 constraints [P_ch[t] P_rate * u_ch[t] for t in range(T)] constraints [P_dis[t] P_rate * (1 - u_ch[t]) for t in range(T)] constraints [u_ch[t] u_dis[t] 1 for t in range(T)] # SOC上下限和末端约束 constraints [SOC_min SOC[t] SOC_max for t in range(T1)] constraints [SOC[T] SOC_init] # 功率平衡 constraints [P_pv_f[t] P_wt_f[t] P_dis[t] P_buy[t] delta_down[t] P_load[t] P_ch[t] P_sell[t] delta_up[t] for t in range(T)] # 需求响应守恒与上下限 constraints [cp.sum(delta_up) cp.sum(delta_down)] constraints [delta_up[t] DR_limit for t in range(T)] constraints [delta_down[t] DR_limit for t in range(T)] # 电网买卖互斥 constraints [P_buy[t] 150 * u_bs[t] for t in range(T)] constraints [P_sell[t] 150 * (1 - u_bs[t]) for t in range(T)]这里有一个细节值得展开。很多人写储能互斥时只写两条M约束但忘记u_ch和u_dis不能同时为1。虽然目标函数里充放电都有运维成本理论上求解器不会主动让两者同时为正但在数值求解过程中一个松弛解可能让u_ch1、u_dis1而两个功率都是0这本身不违规却会给后续灵敏度分析带来麻烦。所以永远加上u_ch u_dis ≤ 1这条约束成本几乎为零但模型更严谨。4.3 目标函数组装与求解器调参# 目标函数 cost cp.sum(c_buy * P_buy - c_sell * P_sell c_DR * (delta_up delta_down) 0.05 * (P_ch P_dis)) objective cp.Minimize(cost) # 求解 prob cp.Problem(objective, constraints) prob.solve(solvercp.HiGHS, verboseFalse)求解器调参方面我的经验是小规模问题用HiGHS默认配置就够了算24时段、48个布尔变量的模型基本是毫秒级。如果模型规模变大可以打开verboseTrue看求解日志重点关注MIP gap是否降到了可接受范围。对于实际项目一般要求MIP gap小于0.1%确保决策结果不是次优解。求解完成后结果都在各变量的value属性里。需要注意如果prob.status不是optimal后面做结果分析前一定要先抛异常否则会把None当成数值去计算很容易出一些非常隐蔽的错误。5. 算例复盘同样的负荷曲线储能和需求响应各省了多少运行费用5.1 三个场景的对比结果为了看清储能和需求响应各自的价值我做了三组对比场景1不加储能、不做需求响应场景2只加储能场景3储能和需求响应都加。三组都使用同一条光伏、风电和负荷曲线结果汇总如下场景购电成本(元)售电收益(元)DR补偿(元)储能运维(元)总成本(元)基线无储能、无需求响应682.5000682.5加储能608.421.6020.0606.8加储能需求响应520.718.024.022.0548.7从总成本看储能单独贡献了约75.7元的节省储能加需求响应合计节省133.8元。需求响应在已有储能的基础上又多省了约58元。这个数字看起来不大但对一个日运行成本几百块的园区来说已经是非常可观的降幅一个月下来就是好几千元。5.2 从调度曲线看储能和DR各自在干什么我提取了场景3的储能SOC曲线和需求响应转移量发现它们的配合很有意思。储能的行为符合预期凌晨0点到5点电价处于谷段时电池从SOC0.2开始充电到早上5点左右充到接近0.85白天光伏出力充足且电价进入平段电池基本不动作甚至有小幅放电帮助光伏消纳晚上17点到22点峰段电价最高电池以约50kW的最大功率放电到22点左右SOC回到0.2附近。整个过程就是一个非常标准的低价充电、高价放电套利行为。需求响应的动作则集中在18点到22点晚高峰每个时段大约下调15kW负荷总量约90kWh。这些被挪走的负荷一部分转移到了凌晨低谷时段一部分转移到了中午光伏大发时段。细心的人会发现场景3的售电收益比场景2少了3.6元原因正是中午原本要卖给电网的光伏电量一部分被转移过来的需求响应负荷就地消纳了。这说明需求响应不仅削峰还能提高光伏的本地消纳率是一举两得的效果。5.3 一个反直觉的发现DR和储能在部分时段会抢电量深入看结果时我发现了一个反直觉的现象需求响应和储能并不总是完全互补在凌晨低谷时段两者都会倾向于增加负荷储能充电、DR转入负荷相当于在竞争低谷电量。如果储能因为SOC上限不能继续充电而DR又想把高峰负荷挪到凌晨就可能出现低谷时段也需要往电网买电的情况。这给运行人员一个直接建议需求响应转移目标不宜和储能充电时段完全重叠。实际项目中可以把DR的转入时段更多地安排在光伏大发的中午让储能专注于凌晨低谷充电这样配合的效率更高。这个发现是单纯看总成本表发现不了的必须深入分析调度结果曲线才能看出来。6. 我踩过的五个坑以及从日前调度向日内滚动扩展的思路6.1 排坑记录单位、M参数、守恒约束和买卖套利第一个坑是SOC递推公式漏乘Δt。最开始我用1小时粒度跑没暴露问题后来想改成15分钟粒度做日内滚动结果SOC一天之内增长了好几倍查了半天才发现公式里少了时间步长。这个问题提醒我哪怕当前场景的Δt等于1也应该把dt变量写在代码里这是一种防御性编程习惯。第二个坑是M参数取得过大。我曾把M设成10000想着越大越不会限制变量结果求解器出现数值病态充电和放电约束在浮点误差层面互相渗透出现了同一时段既充电又放电的幽灵能量。后面我把M改为设备额定功率的两倍比如储能就用100、联络线就用300数值问题立刻消失。M不是越大越好够用就行。第三个坑是需求响应总量守恒。漏掉sum(delta_up) sum(delta_down)时优化成本一下子降得特别低我当时还挺高兴后来一检查发现模型把负荷直接扔掉了一部分没有任何物理机制保证这部分能量守恒。这个坑特别隐蔽因为目标函数值看起来很漂亮但结果完全不可执行。第四个坑是买卖套利。最初我的售电价设为0.45元/kWh低谷购电价只有0.40元/kWh结果模型选择在凌晨大量买电、同时段卖电给电网靠价差凭空赚钱。实际电力交易中这是不允许的后来我加了u_bs互斥变量并把售电价降到0.35元模型才回归正常。第五个坑是储能末端SOC约束。如果不加SOC(T) ≥ SOC(0)模型会倾向于在最后几个时段把电池放空因为放掉的电都是免费的单日成本极低但第二天早上电池没电调度计划根本无法滚动执行。加一条末端约束才让日前计划和实际运行形成闭环。6.2 扩展方向从确定性日前调度走向日内滚动和随机优化这个项目跑通之后最自然的扩展方向是日内滚动调度。日前计划用的预测数据是提前24小时给的实际运行中光伏和负荷都会有偏差把优化时间窗缩短到未来46小时、每15分钟滚动一次能显著提升跟踪精度。滚动调度的代码框架和日前调度几乎一样只是把T从24改成16或24再在每次滚动时把当前SOC作为初始值重新求解。如果想把风光不确定性处理得更严谨可以把确定性优化升级为两阶段随机优化第一阶段决定储能的启停状态和需求响应约定量第二阶段对多个风光出力场景求期望成本最小的购售电决策。这个方向会让模型规模明显变大但HiGHS处理起来仍然可行值得一试。我还试过在约束里加简单的网络潮流约束把单母线模型扩展成多节点配电网模型。那种情况下需要用DistFlow方程建立潮流约束模型会从线性变成二阶锥规划求解器也要从HiGHS换成支持锥规划的求解器。这一步的复杂度提升比较大但如果你手里的微电网由多个台区组成单母线假设会带来不小的误差。这段代码我后来反复改了三个版本印象最深的不是模型本身而是把调度结果真正交给现场运行人员时他们问我的第一个问题永远是明天电池什么时候充满、什么时候允许我手动干预。很多精细化约束在实际落地的第一天就会被简化但这不代表建模没有价值——它给出了一个可比较的基准线也把拍脑袋决定储能策略变成了有依据地调整储能策略。如果现在再让我从头做一次我会先花更多时间在现场数据清洗和负荷特性分析上而不是急着调模型因为输入数据的质量往往比求解器选哪个更能决定调度方案能不能真正落地。