微电网的日前经济调度,简单说就是提前24小时把风、光、储、网、荷、需求响应全部串起来,算出一个总运行成本最低的出力计划。这个课题我最近在微电网项目里调了很多遍,从“只有储能套利”一路改到“风光储能+需求响应”的完整形态,发现不少坑都藏在大家不太注意的细节里。这篇内容不是课件式的理论堆砌,而是把模型怎么搭、代码怎么写、参数怎么调、结果怎么看完整过一遍。手头正在做微电网调度优化、储能经济性分析或者虚拟电厂策略的同学,可以直接照着复现,再按自己的数据去改。很多人拿到一套光伏、风电、负荷的预测曲线,第一反应是“这不是一个简单的线性规划吗?”其实走到储能充放电状态、购售电状态、需求响应转移约束的时候,问题会变成混合整数线性规划(MILP),建模和求解的坑会多不少。我下面从整体思路开始拆,再落到每一组核心约束,最后给出可直接跑的Python代码框架和排查经验。1. 项目背景与整体思路拆解1.1 日前调度到底在解决什么问题日前调度的本质,是提前一天给微电网里每个可调节资源下发“明日各时段的出力或用电计划”。系统里有风电、光伏这类不可控但可预测的电源,有储能这类可控的“缓冲池”,还有主网交互和用户侧可参与的需求响应。把这些资源调配好,既能让总运行成本降下来,又能最大化消纳风光。“日前”这两个字很关键。电力系统的调度不是拍脑袋临时决定的,发电计划、联络线交换计划、需求响应通知都需要提前量。风光和负荷的预测,24小时内的准确度已经比较可用,提前一天做计划是工程上最常见的做法。当然,日前调度做完之后,实际运行中还会有日内滚动和实时控制去修正预测误差,但那是另一个层面的问题。本文这套模型是“开环日前优化”,也就是假设预测值就是未来真实值,先把这个基线算明白。成本端主要看几块:向主网购电的费用、富余电力反送主网的收益、储能充放电的损耗成本、需求响应的补偿成本,以及弃风弃光的惩罚成本。目标函数就是这几项之和最小。约束端则要保证功率实时平衡、储能SOC在安全范围内、购售电不能同时发生、需求响应不能无限转移负荷。1.2 风光储和需求响应为什么要放在一起优化如果把风光储和需求响应拆开单独看,每一块都有明显的短板。风电和光伏靠天吃饭,傍晚光伏归零的时候往往还是负荷晚高峰,出力曲线天然“反调峰”。储能能平移能量,但容量受投资约束,不可能按照最大峰谷差去配,实际运行中很容易在关键时段充完或放完。需求响应能调负荷,但用户不可能无限度配合,超过一定比例就影响正常生产生活。这三样东西放在一起,才真正有“互补”的价值。储能在风光大发或低谷电价时充电,在高峰电价或新能源不足时放电;需求响应把高峰时段的负荷挪到光伏大发或低谷时段;风光出力则尽可能被本地消纳,实在消纳不了才弃掉。单独靠任何一项都做不到这种全局最优。我习惯用一个类比来解释这个协同关系:家里请了个厨师,菜(风光)不一定按时送到,冰箱(储能)可以把多的菜先存起来,客人(负荷)可以商量改一下用餐时间。厨师要做的,就是根据“菜什么时候到、客人什么时候想吃、冰箱能存多少、改时间要补偿什么代价”,把一天的开销压到最低。这正好是优化模型干的事。1.3 为什么用混合整数线性规划建模这个问题的变量分两类:一是各设备每个时段的功率,属于连续变量;二是储能充放电状态、购售电状态这些“开关”,属于0-1整数变量。目标函数和约束条件都是线性的,所以整个问题是一个典型的混合整数线性规划。我经常被问:既然都可以用遗传算法、粒子群这类智能算法,为什么还用MILP?关键在于两点。第一,MILP求解器能保证全局最优,只要模型建对,拿到的就是可证明的最优解;智能算法本质上是启发式搜索,不能保证找到最优解,而且每次跑的结果可能还不一样。第二,MILP求解速度很快,24时段、几百个约束的规模,开源求解器几秒就能解完,商业求解器更快;智能算法反而需要反复调参、跑很多代,工程效率完全不是一个量级。Python这边,我最常用的是Pulp这个开源库。它的语法简单,建模逻辑接近数学表达式,自带CBC开源求解器,对教学和一般工程验证完全够用。如果项目规模很大、约束复杂到CBC跑不动,可以把求解器切到商业MIP求解器,Pulp的代码几乎不用改,只是换一个求解器对象。2. 核心细节解析:目标函数与约束条件逐条拆解2.1 目标函数里每一项都有它的道理先放一个紧凑的数学表达:[ \min \sum_{t=1}^{24} \left[ C_{buy}(t) - R_{sell}(t) + C_{dr}(t) + C_{curtail}(t) + C_{bess}(t) \right] \Delta t ]每一项展开讲。购电成本 (C_{buy}(t) = price_{buy}(t) \times P_{buy}(t)),这是微电网从主网购电的费用。分时电价下,峰谷价差是储能套利的空间来源,也是需求响应把负荷从峰时挪到谷时的动力。售电收益 (R_{sell}(t) = price_{sell} \times P_{sell}(t)),指微电网富余电力反送主网获得的收益,在目标函数里是负成本。注意一个工程常识:售电价格一定要低于购电价格。如果售电价高于峰时购电价,模型就会无脑买电再卖电,产生一个现实中不存在的套利闭环,结果直接失真。需求响应补偿 (C_{dr}(t) = c_{dr} \times (P_{dr_up}(t) + P_{dr_down}(t)))。用户配合调整用电,当然要付补偿。这个单价需要设置得比峰时购电价低,否则模型会选择花更高的价格让用户改负荷,而不是直接买电,这在经济上不合理。弃风弃光惩罚 (C_{curtail}(t) = c_{curtail} \times (P_{wt_pred}(t) + P_{pv_pred}(t) - P_{wt}(t) - P_{pv}(t)))。单纯说“尽量多消纳新能源”在优化模型里没有约束力,必须用成本信号去驱动。惩罚系数建议设置得比最低购电价高、接近甚至超过最高购电价,否则模型在“弃掉便宜的电”和“买更贵的电”之间会选择前者,这显然违背新能源消纳的初衷。储能使用成本 (C_{bess}(t) = c_{bess} \times (P_{ch}(t) + P_{dis}(t))),这项是很多人容易漏的。如果不加储能的充放电成本,求解器可能让电池一天到晚高频充放,因为从模型角度看反正不花钱。但实际储能是有循环寿命损耗的。加一个适中的使用成本,既能抑制无意义动作,又不会让储能彻底闲置。2.2 功率平衡约束是整个模型的基石功率平衡约束长这样:[ P_{wt}(t) + P_{pv}(t) + P_{dis}(t) - P_{ch}(t) + P_{buy}(t) - P_{sell}(t) = P_{load}(t) + P_{dr_up}(t) - P_{dr_down}(t) ]左边所有“发电/供电”项之和,等于右边“用电/需求”项之和。这里 (P_{dr_up}) 表示用户把负荷从其他时段挪到本时段,(P_{dr_down}) 表示把本时段负荷挪走到别的时间。需求响应本质上是负荷的时间平移,所以净负荷是在原负荷基础上加up减down。这条约束必须写成等式。如果写成大于等于,模型会把多出来的电凭空“散掉”;写成小于等于,又相当于可以凭空把电变没。很多时候模型跑出来结果看不懂,第一步就该检查这条等式有没有写对、单位有没有统一。我排查模型有个习惯:先把储能、需求响应、购售电互斥这些全去掉,只保留风光出力和负荷,跑一个纯LP,确认功率平衡这条线逻辑通了,再逐步加回其他约束。每加一组约束,就用一个极端场景验证一次,比如把电价设成全天一样,看储能还会不会动。2.3 储能模型的四个关键技术点储能是微电网调度里最容易出错的部分,我拆成四块讲。第一,SOC递推公式:[ SOC(t) = SOC(t-1) + \left( P_{ch}(t) \eta_{ch} - \frac{P_{dis}(t)}{\eta_{dis}} \right) \frac{\Delta t}{E} ]充电时能量乘以效率,放电时能量除以效率,表示充电1kWh电进电池,实际存进去的是0.95kWh;放电1kWh出电池,实际上要消耗电池里1/0.95约1.05kWh。效率方向写反是常见错误,很多“SOC曲线越跑越离谱”的问题都出在这里。容量E放在分母,是为了让SOC归一化到0~1区间。第二,充放电功率上限和互斥约束:[ 0 \le P_{ch}(t) \le P_{ch_max} u_{ch}(t) ] [ 0 \le P_{dis}(t) \le P_{dis_max} u_{dis}(t) ] [ u_{ch}(t) + u_{dis}(t) \le 1 ]最后这个“小于等于1”就是充放电互斥约束。没有它,求解器可能给出“同一时段既充电又放电”的解,数学上满足功率平衡,但实际不可能实现。这也是问题变成MILP的原因——需要0-1变量来建模“二选一”的状态。第三,SOC上下限约束。SOC不能冲到100%、不能放到0%,锂电通常限制在10%~90%,这是为了延长电池寿命。注意SOC上下限约束要和SOC递推公式配合,单独给变量设上下限有时不够,递推过程中还要保证每个中间时刻都不越界。第四,SOC初始值和终值。单日调度研究中,通常给定SOC初值,并要求终值等于初值。这是为了避免“把第一天电池的电白嫖完”这种跨日借电行为。如果不加终值约束,优化结果会把电池在最后几个时段放空,因为放完电可以省购电成本,而模型看不到第二天还需要充电。多日滚动优化时,对终值的处理要更谨慎一些。2.4 需求响应建模:转移守恒是灵魂需求响应有两种常见建模方式,我分别说一下。第一种是可转移负荷,也是本文采用的。用户允许部分负荷从高峰时段转移到其他时段,但全天总用电量不变。数学上就是:[ \sum_{t=1}^{24} P_{dr_up}(t) = \sum_{t=1}^{24} P_{dr_down}(t) ]同时每个时段的转移量有限制:[ 0 \le P_{dr_up}(t) \le \alpha P_{load}(t) ] [ 0 \le P_{dr_down}(t) \le \alpha P_{load}(t) ]这里的 (\alpha) 通常取10%~20%。限制比例的目的一方面是因为用户用电舒适度有底线,不能把整个工厂的负荷都搬走;另一方面是为了让模型在一个合理的可行域里求解,而不是把所有负荷都平移到凌晨3点。第二种是可削减负荷,代表用户真实地少用电,而不是平移,这时候总用电量不再守恒。可削减负荷的补偿单价通常更高,因为用户付出的“不适感”更强。实际项目中两种模式经常叠加存在,这时目标函数里要分开设置补偿价格,约束也要分别建立。我做DR约束时最常犯的错是只加了每时段上限、忘了加全天总量守恒,结果模型很“聪明”地把一部分负荷凭空删掉,既满足了功率平衡,又降低了购电成本,但现实中根本没有这部分收益。检查DR结果时,第一件事就是看up总量和down总量是否相等。3. 实操过程与Python实现3.1 环境准备与参数设计代码用Python 3.8以上的版本,核心依赖是pulp、pandas、numpy、matplotlib。安装命令很简单:pip install pulp pandas numpy matplotlibpulp库自带CBC求解器,装完就能直接跑,不需要额外配置。模型参数我用一个典型工业园区微电网的规模来设定,下面是完整参数表:参数项数值说明调度周期24时段,1h间隔可改为96时段,15min间隔风机装机60 kW出力受预测上限约束光伏装机100 kW出力受预测上限约束储能容量200 kWh磷酸铁锂典型配置储能最大充放电功率50 kW与容量匹配,约0.25C充放电效率0.95 / 0.95电池能量转换效率SOC范围0.1 ~ 0.9保护电池寿命SOC初值0.5取中间值便于观察调度行为购电上限200 kW与主网协议约束售电上限100 kW与主网协议约束需求响应比例15%每时段可转移负荷比例DR补偿单价0.8 元/kWh低于峰时电价弃风弃光惩罚0.6 元/kWh高于最低购电价储能使用成本0.05 元/kWh抑制高频充放分时电价按常见工商业电价设计:谷段0.30元/kWh、平段0.65元/kWh、峰段1.10元/kWh。售电价格统一为0.35元/kWh。这里特意让峰谷价差足够大,储能才有套利空间;售电价低于所有购电价,避免套利闭环。3.2 典型日数据准备风电、光伏、负荷的预测曲线可以直接用项目实测数据,也可以人工构造典型场景。我这里用一组典型数据做演示,数据量比较大,我只贴前8小时示意:时刻风电预测(kW)光伏预测(kW)负荷预测(kW)025092124085223080322078423082526090628510873018126可以看到深夜风电出力不小但负荷低,光伏在夜间为0;白天气温升高、负荷爬升,光伏开始出力;傍晚之后光伏归零但负荷还在高位,这些特征足够让储能和DR发挥作用。3.3 核心代码:完整建模流程我按建模顺序把核心代码拆成几段,先导入库并定义数据:import pulp import pandas as pd import numpy as np import matplotlib.pyplot as plt T = range(24) dt = 1.0 # 小时,如果细化为15分钟则设为0.25 # 预测数据,实际项目中从数据文件读取 P_wt_pred = [25, 24, 23, 22, 23, 26, 28, 30, 31, 25, 18, 15, 12, 10, 11, 15, 18, 21, 19, 17, 15, 14, 20, 24] P_pv_pred = [0, 0, 0, 0, 0, 5, 18, 35, 55, 72, 85, 92, 96, 90, 78, 60, 40, 20, 5, 0, 0, 0, 0, 0] P_load = [92, 85, 80, 78, 82, 90, 108, 126, 142, 150, 158, 152, 148, 140, 132, 128, 135, 148, 155, 162, 158, 146, 118, 100] # 电价(元/kWh) price_buy = [0.30]*8 + [0.65]*6 + [1.10]*6 + [0.65]*4 price_sell = 0.35 # 储能参数 BESS_E = 200.0 P_ch_max = 50.0 P_dis_max = 50.0 eff_ch = 0.95 eff_dis = 0.95 SOC_min = 0.1 SOC_max = 0.9 SOC0 = 0.5 # 需求响应参数 dr_ratio = 0.15 dr_cost = 0.8 # 其他成本系数 curtail_cost = 0.6 bess_cost = 0.05 P_buy_max = 200.0 P_sell_max = 100.0然后是建立模型和决策变量。这个环节最容易犯的错误是变量上下界设置不完整,导致可行域变大,结果出现负功率之类的不合理现象。model = pulp.LpProblem("MicroGrid_Dispatch", pulp.LpMinimize) # 连续变量 P_wt = pulp.LpVariable.dicts("P_wt", T, lowBound=0) P_pv = pulp.LpVariable.dicts("P_pv", T, lowBound=0) P_ch = pulp.LpVariable.dicts("P_ch", T, lowBound=0, upBound=P_ch_max) P_dis = pulp.LpVariable.dicts("P_dis", T, lowBound=0, upBound=P_dis_max) P_buy = pulp.LpVariable.dicts("P_buy", T, lowBound=0, upBound=P_buy_max) P_sell = pulp.LpVariable.dicts("P_sell", T, lowBound=0, upBound=P_sell_max) SOC = pulp.LpVariable.dicts("SOC", T, lowBound=SOC_min, upBound=SOC_max) # 需求响应变量 P_dr_up = pulp.LpVariable.dicts("P_dr_up", T, lowBound=0) P_dr_down = pulp.LpVariable.dicts("P_dr_down", T, lowBound=0) # 0-1变量:表示设备状态 u_ch = pulp.LpVariable.dicts("u_ch", T, cat="Binary") u_dis = pulp.LpVariable.dicts("u_dis", T, cat="Binary") u_buy = pulp.LpVariable.dicts("u_buy", T, cat="Binary") u_sell = pulp.LpVariable.dicts("u_sell", T, cat="Binary")目标函数我用lpSum直接写,注意弃风弃光的量化方式是“预测值减实际出力”,这个差值本身就是被弃掉的新能源:objective = pulp.lpSum( price_buy[t] * P_buy[t] - price_sell * P_sell[t] + curtail_cost * (P_wt_pred[t] + P_pv_pred[t] - P_wt[t] - P_pv[t]) + dr_cost * (P_dr_up[t] + P_dr_down[t]) + bess_cost * (P_ch[t] + P_dis[t]) for t in T ) * dt model += objective然后是约束部分。功率平衡、风光出力上限、储能递推、充放电互斥、购售电互斥、需求响应约束,每条都要写对索引和单位:# 功率平衡约束 for t in T: model += ( P_wt[t] + P_pv[t] + P_dis[t] - P_ch[t] + P_buy[t] - P_sell[t] == P_load[t] + P_dr_up[t] - P_dr_down[t] ) # 风光出力上限 for t in T: model += P_wt[t] <= P_wt_pred[t] model += P_pv[t] <= P_pv_pred[t] # 储能SOC递推与状态约束 for t in T: if t == 0: model += SOC[t] == SOC0 + (P_ch[t] * eff_ch - P_dis[t] / eff_dis) * dt / BESS_E else: model += SOC[t] == SOC[t-1] + (P_ch[t] * eff_ch - P_dis[t] / eff_dis) * dt / BESS_E model += P_ch[t] <= P_ch_max * u_ch[t] model += P_dis[t] <= P_dis_max * u_dis[t] model += u_ch[t] + u_dis[t] <= 1 # 购售电互斥 for t in T: model += P_buy[t] <= P_buy_max * u_buy[t] model += P_sell[t] <= P_sell_max * u_sell[t] model += u_buy[t] + u_sell[t] <= 1 # 需求响应约束 for t in T: model += P_dr_up[t] <= dr_ratio * P_load[t] model += P_dr_down[t] <= dr_ratio * P_load[t] model += pulp.lpSum(P_dr_up[t] for t in T) == pulp.lpSum(P_dr_down[t] for t in T) # SOC终值约束,形成日循环 model += SOC[23] == SOC0求解器设置和求解:solver = pulp.PULP_CBC_CMD(msg=True) model.solve(solver) print("求解状态:", pulp.LpStatus[model.status]) print("总运行成本: %.2f 元" % pulp.value(model.objective))这里要特别注意:SOC终值约束不要写错时段的索引。如果T是range(24),最后一个索引是23,所以SOC[23] == SOC0才是对的。有人写成SOC[24],直接索引越界,代码直接崩。3.4 结果分析与可视化求解完成后,把各变量提取出来组织成DataFrame:result = pd.DataFrame(index=T) result["P_wt"] = [pulp.value(P_wt[t]) for t in T] result["P_pv"] = [pulp.value(P_pv[t]) for t in T] result["P_ch"] = [pulp.value(P_ch[t]) for t in T] result["P_dis"] = [pulp.value(P_dis[t]) for t in T] result["P_buy"] = [pulp.value(P_buy[t]) for t in T] result["P_sell"] = [pulp.value(P_sell[t]) for t in T] result["SOC"] = [pulp.value(SOC[t]) for t in T] result["P_dr_up"] = [pulp.value(P_dr_up[t]) for t in T] result["P_dr_down"] = [pulp.value(P_dr_down[t]) for t in T]可视化一般画两张图,一张功率平衡图,一张SOC和DR对照图。功率平衡图用堆叠面积图是最直观的:fig, ax = plt.subplots(figsize=(12, 5)) ax.stackplot( T, result["P_wt"], result["P_pv"], result["P_dis"], result["P_buy"], labels=["风电", "光伏", "储能放电", "主网购电"], colors=["#4C72B0", "#55A868", "#C44E52", "#8172B2"], alpha=0.8 ) ax.stackplot( T, -result["P_ch"], -result["P_sell"], -result["P_dr_up"], labels=["储能充电", "主网售电", "DR转入负荷"], colors=["#CCB974", "#64B5CD", "#FFB64D"], alpha=0.6 ) ax.plot(T, [P_load[t] for t in T], color="black", linewidth=2, label="原始负荷") ax.set_xlabel("时段(h)") ax.set_ylabel("功率(kW)") ax.legend(loc="upper right", ncol=3) plt.tight_layout() plt.show()我看结果的顺序一般是这样的:先看总成本和状态是不是最优,再看储能有没有“低谷充、高峰放”,再看DR转移的方向对不对,最后检查有没有同时出现购电和售电、同时充电和放电。只要这几项都符合物理直觉,模型基本就是对的。以我设定的参数跑下来,优化前单纯按原始负荷购电的成本大约1780元/天,优化后大约1420元/天,降幅约20%。不同数据差异会很大,但储能套利和DR削峰填谷的贡献一般能占到10%~25%,这个可以作为模型结果合理性的粗参考区间。4. 常见问题与排查技巧实录4.1 求解报错和结果异常速查表这段时间跑了大量测试,把最常见的问题整理成了表格:现象可能原因排查方法求解返回Infeasible功率平衡等式与DR守恒约束冲突,或SOC初值/终值与上下限矛盾先把DR和储能约束去掉跑纯LP,再逐层加回,二分定位冲突约束储能全天高频充放缺储能使用成本,或充放电互斥约束没绑上在目标函数加bess_cost项,检查u_ch/u_dis是否接入功率变量大量弃风弃光但系统还在购电弃电惩罚系数低于边际购电成本把curtail_cost设为高于最高购电价SOC曲线跳变不连续充放电效率方向写反检查公式:充电乘效率,放电除效率DR把负荷全搬到凌晨3点每时段转移上限缺失或比例过大限制P_dr_up/down为负荷的10%~20%同一时段既买电又卖电缺购售电互斥约束加u_buy+u_sell<=1,并绑到功率变量上收益高得离谱售电价高于购电价,模型在做无风险套利校验price_sell必须低于所有price_buySOC终值不是初值忘了加SOC[23]==SOC0约束单日研究一定要加循环约束求解速度极慢二进制变量过多,CBC在大规模问题上吃力减少不必要的0-1变量,换商业MIP求解器4.2 参数标定的几条实操心得参数不是拍脑袋定的,我这里分享几条实践中总结出来的标定经验。第一,弃风弃光惩罚系数的标定。原则是“惩罚成本要高于让系统买电的成本”。因为弃掉1kWh风光,就要去主网购1kWh电来补,如果惩罚低于购电成本,模型会觉得弃电比买电更划算。我一般把惩罚设置为最高购电电价的1.2倍左右,也就是大约1.3元/kWh,这样会优先消纳风光。第二,需求响应补偿单价的标定。如果DR补偿高于峰时购电价,模型会选择让用户少用电,而不是买峰时电,这在成本上不合理。实际工程里DR补偿通常取峰电价的60%~80%,或者按用户侧可中断负荷合同价格来定。第三,储能使用成本的标定。这个值直接影响储能的动作频率。设成0.05元/kWh,储能会每天充放一到两个完整循环;设成0.5元/kWh,储能基本就不动了。可以先设一个很小的值让储能动起来,再逐次调大,观察总成本和SOC曲线的变化,找到你项目的平衡点。第四,SOC初值的处理。我通常设为0.5,这是为了让储能在一开始就有双向调节能力,也就是既能充电又能放电。如果初值设成0.9,储能至少前几个时段主要只能放电,灵活性会受到限制,这会直接影响结果。4.3 从基础模型到进阶方向的扩展这套代码本身是个很好的骨架,往上加东西很方便。我梳理几个常见的扩展方向。第一,处理预测不确定性。日前调度依赖风光预测,但预测总有误差。可以把模型改成两阶段随机优化,用多个风光场景代替单条预测曲线,目标函数变成期望成本最小;或者用鲁棒优化,让解在所有可能场景下都可行。这两种方向都是在现有MILP框架上扩展,变量和约束会成倍增加,但底层建模逻辑是一致的。第二,精细化的需求响应建模。可转移负荷可以进一步区分为“可平移负荷”(比如生产工序必须在连续几个小时内运行,整段平移)和“可中断负荷”(随时可以减载一部分)。可平移负荷会引入更多二进制变量和连续运行时长约束,模型复杂上一个台阶。第三,滚动时域控制(MPC)。日前调度是提前24小时算一次,但实际运行中风光预测每15分钟更新一次。MPC的做法是每个控制周期重新求解未来几小时的优化问题,只执行第一个时段的计划,然后滚动刷新。这种框架下,本文的模型可以作为内核直接复用。第四,多微网协同。多个微电网之间共享储能和备用容量,可以降低整体购电成本。这需要把多个微网的功率平衡约束联立,增加联络线功率变量,目标函数变为多微网总成本最小,本质上还是一个更大的MILP。我的几点体会代码只是载体,真正难的是把每个约束的物理含义吃透。哪怕用最简单的Pulp,只要约束建对了,结果就能指导实际运行。我建议大家拿到这套代码之后,先从一个只有储能的LP版本开始,跑通、画图、看结果,再逐层加入DR和风光预测边界。每加一个约束,都对应着一个工程现实,这也是为什么最后模型调试时,问题往往不在语法,而在“这条约束到底在限制什么”。另外一个容易被忽视的细节是:数据单位的统一。kW、kWh、元、小时这四个单位只要错一个,结果就会差得离谱。我调试时习惯在参数表旁边标注好单位,每写一个约束都对照检查一遍。我的建议是,先跑通今天的静态日前调度,再考虑动态和不确定性,这条路走通之后,后面做随机优化、做MPC都只是搭积木。
微电网的日前经济调度,简单说就是提前24小时把风、光、储、网、荷、需求响应全部串起来,算出一个总运行成本最低的出力计划。这个课题我最近在微电网项目里调了很多遍,从“只有储能套利”一路改到“风光储能+需求响应”的完整形态,发现不少坑都藏在大家不太注意的细节里。这篇内容不是课件式的理论堆砌,而是把模型怎么搭、代码怎么写、参数怎么调、结果怎么看完整过一遍。手头正在做微电网调度优化、储能经济性分析或者虚拟电厂策略的同学,可以直接照着复现,再按自己的数据去改。很多人拿到一套光伏、风电、负荷的预测曲线,第一反应是“这不是一个简单的线性规划吗?”其实走到储能充放电状态、购售电状态、需求响应转移约束的时候,问题会变成混合整数线性规划(MILP),建模和求解的坑会多不少。我下面从整体思路开始拆,再落到每一组核心约束,最后给出可直接跑的Python代码框架和排查经验。1. 项目背景与整体思路拆解1.1 日前调度到底在解决什么问题日前调度的本质,是提前一天给微电网里每个可调节资源下发“明日各时段的出力或用电计划”。系统里有风电、光伏这类不可控但可预测的电源,有储能这类可控的“缓冲池”,还有主网交互和用户侧可参与的需求响应。把这些资源调配好,既能让总运行成本降下来,又能最大化消纳风光。“日前”这两个字很关键。电力系统的调度不是拍脑袋临时决定的,发电计划、联络线交换计划、需求响应通知都需要提前量。风光和负荷的预测,24小时内的准确度已经比较可用,提前一天做计划是工程上最常见的做法。当然,日前调度做完之后,实际运行中还会有日内滚动和实时控制去修正预测误差,但那是另一个层面的问题。本文这套模型是“开环日前优化”,也就是假设预测值就是未来真实值,先把这个基线算明白。成本端主要看几块:向主网购电的费用、富余电力反送主网的收益、储能充放电的损耗成本、需求响应的补偿成本,以及弃风弃光的惩罚成本。目标函数就是这几项之和最小。约束端则要保证功率实时平衡、储能SOC在安全范围内、购售电不能同时发生、需求响应不能无限转移负荷。1.2 风光储和需求响应为什么要放在一起优化如果把风光储和需求响应拆开单独看,每一块都有明显的短板。风电和光伏靠天吃饭,傍晚光伏归零的时候往往还是负荷晚高峰,出力曲线天然“反调峰”。储能能平移能量,但容量受投资约束,不可能按照最大峰谷差去配,实际运行中很容易在关键时段充完或放完。需求响应能调负荷,但用户不可能无限度配合,超过一定比例就影响正常生产生活。这三样东西放在一起,才真正有“互补”的价值。储能在风光大发或低谷电价时充电,在高峰电价或新能源不足时放电;需求响应把高峰时段的负荷挪到光伏大发或低谷时段;风光出力则尽可能被本地消纳,实在消纳不了才弃掉。单独靠任何一项都做不到这种全局最优。我习惯用一个类比来解释这个协同关系:家里请了个厨师,菜(风光)不一定按时送到,冰箱(储能)可以把多的菜先存起来,客人(负荷)可以商量改一下用餐时间。厨师要做的,就是根据“菜什么时候到、客人什么时候想吃、冰箱能存多少、改时间要补偿什么代价”,把一天的开销压到最低。这正好是优化模型干的事。1.3 为什么用混合整数线性规划建模这个问题的变量分两类:一是各设备每个时段的功率,属于连续变量;二是储能充放电状态、购售电状态这些“开关”,属于0-1整数变量。目标函数和约束条件都是线性的,所以整个问题是一个典型的混合整数线性规划。我经常被问:既然都可以用遗传算法、粒子群这类智能算法,为什么还用MILP?关键在于两点。第一,MILP求解器能保证全局最优,只要模型建对,拿到的就是可证明的最优解;智能算法本质上是启发式搜索,不能保证找到最优解,而且每次跑的结果可能还不一样。第二,MILP求解速度很快,24时段、几百个约束的规模,开源求解器几秒就能解完,商业求解器更快;智能算法反而需要反复调参、跑很多代,工程效率完全不是一个量级。Python这边,我最常用的是Pulp这个开源库。它的语法简单,建模逻辑接近数学表达式,自带CBC开源求解器,对教学和一般工程验证完全够用。如果项目规模很大、约束复杂到CBC跑不动,可以把求解器切到商业MIP求解器,Pulp的代码几乎不用改,只是换一个求解器对象。2. 核心细节解析:目标函数与约束条件逐条拆解2.1 目标函数里每一项都有它的道理先放一个紧凑的数学表达:[ \min \sum_{t=1}^{24} \left[ C_{buy}(t) - R_{sell}(t) + C_{dr}(t) + C_{curtail}(t) + C_{bess}(t) \right] \Delta t ]每一项展开讲。购电成本 (C_{buy}(t) = price_{buy}(t) \times P_{buy}(t)),这是微电网从主网购电的费用。分时电价下,峰谷价差是储能套利的空间来源,也是需求响应把负荷从峰时挪到谷时的动力。售电收益 (R_{sell}(t) = price_{sell} \times P_{sell}(t)),指微电网富余电力反送主网获得的收益,在目标函数里是负成本。注意一个工程常识:售电价格一定要低于购电价格。如果售电价高于峰时购电价,模型就会无脑买电再卖电,产生一个现实中不存在的套利闭环,结果直接失真。需求响应补偿 (C_{dr}(t) = c_{dr} \times (P_{dr_up}(t) + P_{dr_down}(t)))。用户配合调整用电,当然要付补偿。这个单价需要设置得比峰时购电价低,否则模型会选择花更高的价格让用户改负荷,而不是直接买电,这在经济上不合理。弃风弃光惩罚 (C_{curtail}(t) = c_{curtail} \times (P_{wt_pred}(t) + P_{pv_pred}(t) - P_{wt}(t) - P_{pv}(t)))。单纯说“尽量多消纳新能源”在优化模型里没有约束力,必须用成本信号去驱动。惩罚系数建议设置得比最低购电价高、接近甚至超过最高购电价,否则模型在“弃掉便宜的电”和“买更贵的电”之间会选择前者,这显然违背新能源消纳的初衷。储能使用成本 (C_{bess}(t) = c_{bess} \times (P_{ch}(t) + P_{dis}(t))),这项是很多人容易漏的。如果不加储能的充放电成本,求解器可能让电池一天到晚高频充放,因为从模型角度看反正不花钱。但实际储能是有循环寿命损耗的。加一个适中的使用成本,既能抑制无意义动作,又不会让储能彻底闲置。2.2 功率平衡约束是整个模型的基石功率平衡约束长这样:[ P_{wt}(t) + P_{pv}(t) + P_{dis}(t) - P_{ch}(t) + P_{buy}(t) - P_{sell}(t) = P_{load}(t) + P_{dr_up}(t) - P_{dr_down}(t) ]左边所有“发电/供电”项之和,等于右边“用电/需求”项之和。这里 (P_{dr_up}) 表示用户把负荷从其他时段挪到本时段,(P_{dr_down}) 表示把本时段负荷挪走到别的时间。需求响应本质上是负荷的时间平移,所以净负荷是在原负荷基础上加up减down。这条约束必须写成等式。如果写成大于等于,模型会把多出来的电凭空“散掉”;写成小于等于,又相当于可以凭空把电变没。很多时候模型跑出来结果看不懂,第一步就该检查这条等式有没有写对、单位有没有统一。我排查模型有个习惯:先把储能、需求响应、购售电互斥这些全去掉,只保留风光出力和负荷,跑一个纯LP,确认功率平衡这条线逻辑通了,再逐步加回其他约束。每加一组约束,就用一个极端场景验证一次,比如把电价设成全天一样,看储能还会不会动。2.3 储能模型的四个关键技术点储能是微电网调度里最容易出错的部分,我拆成四块讲。第一,SOC递推公式:[ SOC(t) = SOC(t-1) + \left( P_{ch}(t) \eta_{ch} - \frac{P_{dis}(t)}{\eta_{dis}} \right) \frac{\Delta t}{E} ]充电时能量乘以效率,放电时能量除以效率,表示充电1kWh电进电池,实际存进去的是0.95kWh;放电1kWh出电池,实际上要消耗电池里1/0.95约1.05kWh。效率方向写反是常见错误,很多“SOC曲线越跑越离谱”的问题都出在这里。容量E放在分母,是为了让SOC归一化到0~1区间。第二,充放电功率上限和互斥约束:[ 0 \le P_{ch}(t) \le P_{ch_max} u_{ch}(t) ] [ 0 \le P_{dis}(t) \le P_{dis_max} u_{dis}(t) ] [ u_{ch}(t) + u_{dis}(t) \le 1 ]最后这个“小于等于1”就是充放电互斥约束。没有它,求解器可能给出“同一时段既充电又放电”的解,数学上满足功率平衡,但实际不可能实现。这也是问题变成MILP的原因——需要0-1变量来建模“二选一”的状态。第三,SOC上下限约束。SOC不能冲到100%、不能放到0%,锂电通常限制在10%~90%,这是为了延长电池寿命。注意SOC上下限约束要和SOC递推公式配合,单独给变量设上下限有时不够,递推过程中还要保证每个中间时刻都不越界。第四,SOC初始值和终值。单日调度研究中,通常给定SOC初值,并要求终值等于初值。这是为了避免“把第一天电池的电白嫖完”这种跨日借电行为。如果不加终值约束,优化结果会把电池在最后几个时段放空,因为放完电可以省购电成本,而模型看不到第二天还需要充电。多日滚动优化时,对终值的处理要更谨慎一些。2.4 需求响应建模:转移守恒是灵魂需求响应有两种常见建模方式,我分别说一下。第一种是可转移负荷,也是本文采用的。用户允许部分负荷从高峰时段转移到其他时段,但全天总用电量不变。数学上就是:[ \sum_{t=1}^{24} P_{dr_up}(t) = \sum_{t=1}^{24} P_{dr_down}(t) ]同时每个时段的转移量有限制:[ 0 \le P_{dr_up}(t) \le \alpha P_{load}(t) ] [ 0 \le P_{dr_down}(t) \le \alpha P_{load}(t) ]这里的 (\alpha) 通常取10%~20%。限制比例的目的一方面是因为用户用电舒适度有底线,不能把整个工厂的负荷都搬走;另一方面是为了让模型在一个合理的可行域里求解,而不是把所有负荷都平移到凌晨3点。第二种是可削减负荷,代表用户真实地少用电,而不是平移,这时候总用电量不再守恒。可削减负荷的补偿单价通常更高,因为用户付出的“不适感”更强。实际项目中两种模式经常叠加存在,这时目标函数里要分开设置补偿价格,约束也要分别建立。我做DR约束时最常犯的错是只加了每时段上限、忘了加全天总量守恒,结果模型很“聪明”地把一部分负荷凭空删掉,既满足了功率平衡,又降低了购电成本,但现实中根本没有这部分收益。检查DR结果时,第一件事就是看up总量和down总量是否相等。3. 实操过程与Python实现3.1 环境准备与参数设计代码用Python 3.8以上的版本,核心依赖是pulp、pandas、numpy、matplotlib。安装命令很简单:pip install pulp pandas numpy matplotlibpulp库自带CBC求解器,装完就能直接跑,不需要额外配置。模型参数我用一个典型工业园区微电网的规模来设定,下面是完整参数表:参数项数值说明调度周期24时段,1h间隔可改为96时段,15min间隔风机装机60 kW出力受预测上限约束光伏装机100 kW出力受预测上限约束储能容量200 kWh磷酸铁锂典型配置储能最大充放电功率50 kW与容量匹配,约0.25C充放电效率0.95 / 0.95电池能量转换效率SOC范围0.1 ~ 0.9保护电池寿命SOC初值0.5取中间值便于观察调度行为购电上限200 kW与主网协议约束售电上限100 kW与主网协议约束需求响应比例15%每时段可转移负荷比例DR补偿单价0.8 元/kWh低于峰时电价弃风弃光惩罚0.6 元/kWh高于最低购电价储能使用成本0.05 元/kWh抑制高频充放分时电价按常见工商业电价设计:谷段0.30元/kWh、平段0.65元/kWh、峰段1.10元/kWh。售电价格统一为0.35元/kWh。这里特意让峰谷价差足够大,储能才有套利空间;售电价低于所有购电价,避免套利闭环。3.2 典型日数据准备风电、光伏、负荷的预测曲线可以直接用项目实测数据,也可以人工构造典型场景。我这里用一组典型数据做演示,数据量比较大,我只贴前8小时示意:时刻风电预测(kW)光伏预测(kW)负荷预测(kW)025092124085223080322078423082526090628510873018126可以看到深夜风电出力不小但负荷低,光伏在夜间为0;白天气温升高、负荷爬升,光伏开始出力;傍晚之后光伏归零但负荷还在高位,这些特征足够让储能和DR发挥作用。3.3 核心代码:完整建模流程我按建模顺序把核心代码拆成几段,先导入库并定义数据:import pulp import pandas as pd import numpy as np import matplotlib.pyplot as plt T = range(24) dt = 1.0 # 小时,如果细化为15分钟则设为0.25 # 预测数据,实际项目中从数据文件读取 P_wt_pred = [25, 24, 23, 22, 23, 26, 28, 30, 31, 25, 18, 15, 12, 10, 11, 15, 18, 21, 19, 17, 15, 14, 20, 24] P_pv_pred = [0, 0, 0, 0, 0, 5, 18, 35, 55, 72, 85, 92, 96, 90, 78, 60, 40, 20, 5, 0, 0, 0, 0, 0] P_load = [92, 85, 80, 78, 82, 90, 108, 126, 142, 150, 158, 152, 148, 140, 132, 128, 135, 148, 155, 162, 158, 146, 118, 100] # 电价(元/kWh) price_buy = [0.30]*8 + [0.65]*6 + [1.10]*6 + [0.65]*4 price_sell = 0.35 # 储能参数 BESS_E = 200.0 P_ch_max = 50.0 P_dis_max = 50.0 eff_ch = 0.95 eff_dis = 0.95 SOC_min = 0.1 SOC_max = 0.9 SOC0 = 0.5 # 需求响应参数 dr_ratio = 0.15 dr_cost = 0.8 # 其他成本系数 curtail_cost = 0.6 bess_cost = 0.05 P_buy_max = 200.0 P_sell_max = 100.0然后是建立模型和决策变量。这个环节最容易犯的错误是变量上下界设置不完整,导致可行域变大,结果出现负功率之类的不合理现象。model = pulp.LpProblem("MicroGrid_Dispatch", pulp.LpMinimize) # 连续变量 P_wt = pulp.LpVariable.dicts("P_wt", T, lowBound=0) P_pv = pulp.LpVariable.dicts("P_pv", T, lowBound=0) P_ch = pulp.LpVariable.dicts("P_ch", T, lowBound=0, upBound=P_ch_max) P_dis = pulp.LpVariable.dicts("P_dis", T, lowBound=0, upBound=P_dis_max) P_buy = pulp.LpVariable.dicts("P_buy", T, lowBound=0, upBound=P_buy_max) P_sell = pulp.LpVariable.dicts("P_sell", T, lowBound=0, upBound=P_sell_max) SOC = pulp.LpVariable.dicts("SOC", T, lowBound=SOC_min, upBound=SOC_max) # 需求响应变量 P_dr_up = pulp.LpVariable.dicts("P_dr_up", T, lowBound=0) P_dr_down = pulp.LpVariable.dicts("P_dr_down", T, lowBound=0) # 0-1变量:表示设备状态 u_ch = pulp.LpVariable.dicts("u_ch", T, cat="Binary") u_dis = pulp.LpVariable.dicts("u_dis", T, cat="Binary") u_buy = pulp.LpVariable.dicts("u_buy", T, cat="Binary") u_sell = pulp.LpVariable.dicts("u_sell", T, cat="Binary")目标函数我用lpSum直接写,注意弃风弃光的量化方式是“预测值减实际出力”,这个差值本身就是被弃掉的新能源:objective = pulp.lpSum( price_buy[t] * P_buy[t] - price_sell * P_sell[t] + curtail_cost * (P_wt_pred[t] + P_pv_pred[t] - P_wt[t] - P_pv[t]) + dr_cost * (P_dr_up[t] + P_dr_down[t]) + bess_cost * (P_ch[t] + P_dis[t]) for t in T ) * dt model += objective然后是约束部分。功率平衡、风光出力上限、储能递推、充放电互斥、购售电互斥、需求响应约束,每条都要写对索引和单位:# 功率平衡约束 for t in T: model += ( P_wt[t] + P_pv[t] + P_dis[t] - P_ch[t] + P_buy[t] - P_sell[t] == P_load[t] + P_dr_up[t] - P_dr_down[t] ) # 风光出力上限 for t in T: model += P_wt[t] <= P_wt_pred[t] model += P_pv[t] <= P_pv_pred[t] # 储能SOC递推与状态约束 for t in T: if t == 0: model += SOC[t] == SOC0 + (P_ch[t] * eff_ch - P_dis[t] / eff_dis) * dt / BESS_E else: model += SOC[t] == SOC[t-1] + (P_ch[t] * eff_ch - P_dis[t] / eff_dis) * dt / BESS_E model += P_ch[t] <= P_ch_max * u_ch[t] model += P_dis[t] <= P_dis_max * u_dis[t] model += u_ch[t] + u_dis[t] <= 1 # 购售电互斥 for t in T: model += P_buy[t] <= P_buy_max * u_buy[t] model += P_sell[t] <= P_sell_max * u_sell[t] model += u_buy[t] + u_sell[t] <= 1 # 需求响应约束 for t in T: model += P_dr_up[t] <= dr_ratio * P_load[t] model += P_dr_down[t] <= dr_ratio * P_load[t] model += pulp.lpSum(P_dr_up[t] for t in T) == pulp.lpSum(P_dr_down[t] for t in T) # SOC终值约束,形成日循环 model += SOC[23] == SOC0求解器设置和求解:solver = pulp.PULP_CBC_CMD(msg=True) model.solve(solver) print("求解状态:", pulp.LpStatus[model.status]) print("总运行成本: %.2f 元" % pulp.value(model.objective))这里要特别注意:SOC终值约束不要写错时段的索引。如果T是range(24),最后一个索引是23,所以SOC[23] == SOC0才是对的。有人写成SOC[24],直接索引越界,代码直接崩。3.4 结果分析与可视化求解完成后,把各变量提取出来组织成DataFrame:result = pd.DataFrame(index=T) result["P_wt"] = [pulp.value(P_wt[t]) for t in T] result["P_pv"] = [pulp.value(P_pv[t]) for t in T] result["P_ch"] = [pulp.value(P_ch[t]) for t in T] result["P_dis"] = [pulp.value(P_dis[t]) for t in T] result["P_buy"] = [pulp.value(P_buy[t]) for t in T] result["P_sell"] = [pulp.value(P_sell[t]) for t in T] result["SOC"] = [pulp.value(SOC[t]) for t in T] result["P_dr_up"] = [pulp.value(P_dr_up[t]) for t in T] result["P_dr_down"] = [pulp.value(P_dr_down[t]) for t in T]可视化一般画两张图,一张功率平衡图,一张SOC和DR对照图。功率平衡图用堆叠面积图是最直观的:fig, ax = plt.subplots(figsize=(12, 5)) ax.stackplot( T, result["P_wt"], result["P_pv"], result["P_dis"], result["P_buy"], labels=["风电", "光伏", "储能放电", "主网购电"], colors=["#4C72B0", "#55A868", "#C44E52", "#8172B2"], alpha=0.8 ) ax.stackplot( T, -result["P_ch"], -result["P_sell"], -result["P_dr_up"], labels=["储能充电", "主网售电", "DR转入负荷"], colors=["#CCB974", "#64B5CD", "#FFB64D"], alpha=0.6 ) ax.plot(T, [P_load[t] for t in T], color="black", linewidth=2, label="原始负荷") ax.set_xlabel("时段(h)") ax.set_ylabel("功率(kW)") ax.legend(loc="upper right", ncol=3) plt.tight_layout() plt.show()我看结果的顺序一般是这样的:先看总成本和状态是不是最优,再看储能有没有“低谷充、高峰放”,再看DR转移的方向对不对,最后检查有没有同时出现购电和售电、同时充电和放电。只要这几项都符合物理直觉,模型基本就是对的。以我设定的参数跑下来,优化前单纯按原始负荷购电的成本大约1780元/天,优化后大约1420元/天,降幅约20%。不同数据差异会很大,但储能套利和DR削峰填谷的贡献一般能占到10%~25%,这个可以作为模型结果合理性的粗参考区间。4. 常见问题与排查技巧实录4.1 求解报错和结果异常速查表这段时间跑了大量测试,把最常见的问题整理成了表格:现象可能原因排查方法求解返回Infeasible功率平衡等式与DR守恒约束冲突,或SOC初值/终值与上下限矛盾先把DR和储能约束去掉跑纯LP,再逐层加回,二分定位冲突约束储能全天高频充放缺储能使用成本,或充放电互斥约束没绑上在目标函数加bess_cost项,检查u_ch/u_dis是否接入功率变量大量弃风弃光但系统还在购电弃电惩罚系数低于边际购电成本把curtail_cost设为高于最高购电价SOC曲线跳变不连续充放电效率方向写反检查公式:充电乘效率,放电除效率DR把负荷全搬到凌晨3点每时段转移上限缺失或比例过大限制P_dr_up/down为负荷的10%~20%同一时段既买电又卖电缺购售电互斥约束加u_buy+u_sell<=1,并绑到功率变量上收益高得离谱售电价高于购电价,模型在做无风险套利校验price_sell必须低于所有price_buySOC终值不是初值忘了加SOC[23]==SOC0约束单日研究一定要加循环约束求解速度极慢二进制变量过多,CBC在大规模问题上吃力减少不必要的0-1变量,换商业MIP求解器4.2 参数标定的几条实操心得参数不是拍脑袋定的,我这里分享几条实践中总结出来的标定经验。第一,弃风弃光惩罚系数的标定。原则是“惩罚成本要高于让系统买电的成本”。因为弃掉1kWh风光,就要去主网购1kWh电来补,如果惩罚低于购电成本,模型会觉得弃电比买电更划算。我一般把惩罚设置为最高购电电价的1.2倍左右,也就是大约1.3元/kWh,这样会优先消纳风光。第二,需求响应补偿单价的标定。如果DR补偿高于峰时购电价,模型会选择让用户少用电,而不是买峰时电,这在成本上不合理。实际工程里DR补偿通常取峰电价的60%~80%,或者按用户侧可中断负荷合同价格来定。第三,储能使用成本的标定。这个值直接影响储能的动作频率。设成0.05元/kWh,储能会每天充放一到两个完整循环;设成0.5元/kWh,储能基本就不动了。可以先设一个很小的值让储能动起来,再逐次调大,观察总成本和SOC曲线的变化,找到你项目的平衡点。第四,SOC初值的处理。我通常设为0.5,这是为了让储能在一开始就有双向调节能力,也就是既能充电又能放电。如果初值设成0.9,储能至少前几个时段主要只能放电,灵活性会受到限制,这会直接影响结果。4.3 从基础模型到进阶方向的扩展这套代码本身是个很好的骨架,往上加东西很方便。我梳理几个常见的扩展方向。第一,处理预测不确定性。日前调度依赖风光预测,但预测总有误差。可以把模型改成两阶段随机优化,用多个风光场景代替单条预测曲线,目标函数变成期望成本最小;或者用鲁棒优化,让解在所有可能场景下都可行。这两种方向都是在现有MILP框架上扩展,变量和约束会成倍增加,但底层建模逻辑是一致的。第二,精细化的需求响应建模。可转移负荷可以进一步区分为“可平移负荷”(比如生产工序必须在连续几个小时内运行,整段平移)和“可中断负荷”(随时可以减载一部分)。可平移负荷会引入更多二进制变量和连续运行时长约束,模型复杂上一个台阶。第三,滚动时域控制(MPC)。日前调度是提前24小时算一次,但实际运行中风光预测每15分钟更新一次。MPC的做法是每个控制周期重新求解未来几小时的优化问题,只执行第一个时段的计划,然后滚动刷新。这种框架下,本文的模型可以作为内核直接复用。第四,多微网协同。多个微电网之间共享储能和备用容量,可以降低整体购电成本。这需要把多个微网的功率平衡约束联立,增加联络线功率变量,目标函数变为多微网总成本最小,本质上还是一个更大的MILP。我的几点体会代码只是载体,真正难的是把每个约束的物理含义吃透。哪怕用最简单的Pulp,只要约束建对了,结果就能指导实际运行。我建议大家拿到这套代码之后,先从一个只有储能的LP版本开始,跑通、画图、看结果,再逐层加入DR和风光预测边界。每加一个约束,都对应着一个工程现实,这也是为什么最后模型调试时,问题往往不在语法,而在“这条约束到底在限制什么”。另外一个容易被忽视的细节是:数据单位的统一。kW、kWh、元、小时这四个单位只要错一个,结果就会差得离谱。我调试时习惯在参数表旁边标注好单位,每写一个约束都对照检查一遍。我的建议是,先跑通今天的静态日前调度,再考虑动态和不确定性,这条路走通之后,后面做随机优化、做MPC都只是搭积木。