做微电网调度这几年,我最大的感受是:模型本身并不复杂,真正卡住人的往往是那些“看似不起眼”的细节——储能SOC到底怎么更新才不会越界?可转移负荷的总量守恒约束怎么加才不至于让模型直接无解?风光出力的预测曲线到底怎么处理才能避免调度结果“只存在于论文里”?如果你也在纠结这些问题,那这篇笔记应该能帮到你。
这篇文章围绕“基于风光储能和需求响应的微电网日前经济调度”,把Matlab代码实现从建模思路、约束推导到排错经验完整过一遍。内容按实战来写,适合正在做微电网调度、综合能源系统优化、或者毕业设计需要复现同类代码的同学参考。我会把每一步为什么这么设计、约束为什么这么写、代码哪里容易爆雷都讲清楚。
1. 日前经济调度的整体思路拆解
1.1 这个调度问题到底在“优化”什么
微电网日前经济调度,说白了就是提前一天把第二天的运行计划定下来:每个时段光伏出多少、风机出多少、储能是充电还是放电、跟大电网买多少电、哪些柔性负荷可以挪一挪位置。目标只有一个——在满足用电需求的前提下,让第二天的总运行成本尽可能低。
但这里有个容易被忽略的点:这是“日前”调度,不是“实时”调度。日前意味着我们用的是预测数据,比如明天的光伏预测曲线、风电预测曲线、负荷预测曲线。既然是预测,就一定有误差,所以调度结果必须留有余地。这也是为什么很多代码里会加旋转备用约束——就是为了兜住预测误差带来的风险。比如某时段预测光伏出力是100kW,但实际可能只有80kW,那储能和主网购电就得有足够的上调空间把这20kW补上。
从数学上讲,这是一个带约束的优化问题。决策变量包括储能各时段充放电功率、与主网的交换功率、可转移负荷的调整量、可中断负荷的削减量等。目标函数是所有成本项的总和最小化。约束条件包括功率平衡、储能状态约束、负荷调整边界、电网交互上下限等。整套东西形成一个标准的线性规划(LP)或者混合整数线性规划(MILP),求解器解出来的就是最优调度方案。
1.2 为什么风光储和需求响应要放在一个框架里看
先说储能。光伏夜间出力为零,风电又有很强的间歇性,如果光靠预测曲线来安排运行,出力波动一大,电网就得频繁调峰。储能的角色是“缓冲池”:白天光伏大发时把多余电量存起来,晚高峰时再放出去,既减少了向大电网的购电费用,也平抑了功率波动。
再说需求响应。储能只能解决“电量平移”的问题,但负荷本身如果也具备调节能力,整个系统的灵活性就会上一个台阶。需求响应一般分两类:一类是可转移负荷,比如洗衣机、蓄热式电采暖、工业中的可间歇流程,这类负荷的特点是总量不变,但可以在时间轴上平移;另一类是可中断负荷,比如非关键辅助设备,这类负荷响应速度最快,但削减的是真实用电量,需要支付补偿费用。
所以整体框架是这样的:风光提供“便宜但随机”的电量,储能提供“双向调节”的能力,需求响应提供“负荷侧配合”的余量,再加上与主网的交易作为最终兜底手段。四者配合,才能在成本和可靠性之间找到平衡点。这也是为什么做日前调度时,不能单独看某一类资源,必须把它们统一进一个优化模型里协同求解。
1.3 用Matlab实现时的技术路线选型
Matlab里做优化调度,主流的方案有这么几条:
一是用自带的linprog(线性规划)或intlinprog(混合整数线性规划)函数。优点是零安装成本,Matlab自带;缺点是建模自由度低,约束一多代码就变得很长且难调试。
二是用Yalmip工具箱建模,后端接Cplex、Gurobi这类商业求解器,或者Matlab自带的求解器。这是目前科研和工程里最常见的组合。Yalmip的建模语法和数学表达式几乎一一对应,比如约束条件直接用“>=”写进去,目标函数直接用表达式加和,代码可读性非常高。缺点是Cplex和Gurbi是商业软件,需要额外安装授权。
三是用Matlab的Optimization Toolbox里的prob2struct等结构化建模方式,或者用CVX等凸优化工具箱。这些各有适用场景,但对于标准的微电网日前经济调度,我个人的建议是:直接用Yalmip作为建模层,求解器优先接Cplex,因为CPLEX处理MILP非常稳定。实在没有Cplex,用Matlab自带的intlinprog也能跑,对于维度不大的微电网问题(比如24时段)完全够用。
选型时要考虑一个实际问题:如果要做的是可转移负荷,意味着要引入0-1变量(判断负荷是否发生转移),整个问题就从LP变成了MILP。MILP的求解复杂度和决策变量数量直接相关,如果调度时段是96点(15分钟一个点),0-1变量的数量会明显膨胀,这时候求解器的性能差异就很明显了。
2. 核心环节解析与实操要点
2.1 目标函数:每一项成本都要有来源
经济调度的目标函数是整个模型的“指挥棒”,每一项成本必须能解释清楚它的实际含义。我见过太多代码把所有成本堆在一起,结果某个权重大到失真,调度结果完全被该权重带着走。
以典型的风光储+需求响应系统为例,目标函数通常包含这几项:
购电成本是主项。微电网与主网交换功率时,买电需要付费,有些场景下允许向主网售电,则售电产生收益。这里要注意购电和售电价格往往不同,而且分时电价下各时段价格也各不相同。代码用价格向量price_buy和price_sell分别表示各时段购售电价,与交换功率相乘再求和。
储能折旧成本也常被计入。储能电池每充放一次都会产生损耗,简单处理方式是给单位充放电电量一个损耗系数,比如每kWh充放电计0.02元折旧费。这样模型不会为了“好玩”频繁充放电,而会主动追求有效率地利用储能。
需求响应成本是第三项。可转移负荷和可中断负荷都需要给用户补偿。可转移负荷通常按转移电量乘以单位补偿单价计算,可中断负荷按削减电量乘以中断补偿单价计算。补偿单价一般低于购电价格,否则调用需求响应反而亏钱,这在经济上就没有意义了。
弃风弃光惩罚也很常见。光伏和风电虽然是免费能源,但如果没有储能在场,某些时段出力确实用不完,直接弃掉反而比让储能充满再放出来更划算。但为了鼓励新能源消纳,会给弃风弃光量设一个较大的惩罚系数。这个系数的大小很讲究,设得太大,模型会不惜成本把电存起来;设得太小,模型会倾向直接弃电。
逐项列出来就是这样:
| 成本项 | 计算公式 | 说明 |
|---|---|---|
| 购电成本 | sum(buy_price(t) * P_buy(t) * dt) | 各时段购电量乘对应电价 |
| 售电收益 | sum(sell_price(t) * P_sell(t) * dt) | 余电上网时产生负成本(收益) |
| 储能损耗 | sum(deg_rate * (P_dis + P_ch) * dt) | 统一按充放电电量计损耗 |
| 可转移负荷补偿 | sum(price_trans * P_shift(t) * dt) | 转移电量乘补偿单价 |
| 可中断负荷补偿 | sum(price_inter * P_inter(t) * dt) | 削减电量乘补偿单价 |
| 弃风弃光惩罚 | sum(penalty * (P_curtail_w + P_curtail_pv) * dt) | 惩罚系数乘弃电量 |
这里有个实操细节:所有的能量型变量(功率)乘以时段步长dt之后才变成电量,成本算的是电量和单价的乘积,所以dt必须统一。24时段调度dt=1小时,96时段调度dt=0.25小时。这个看起来简单,但很多人算出来的成本数字不合理,十有八九是dt没乘或者乘错了地方。
2.2 功率平衡与储能约束:最容易写错的地方
功率平衡约束是模型的物理基础:每个时段,系统内所有电源出力加上购电量,必须等于所有负荷加上售电量,再加上储能充电功率。写成表达式就是:
P_pv(t) + P_wind(t) + P_dis(t) + P_buy(t) = P_load(t) + P_ch(t) + P_sell(t) + P_shift_in(t) + P_inter(t)
这里需要特别注意P_shift_in是“转入”的负荷。可转移负荷的本质是负荷在时间上的移动,所以如果某时段的负荷因为转移而增加了,这个增加量要体现在功率平衡方程式右边。相应地,转出负荷的时段,负荷会减少。
储能约束是个魔鬼细节集中的地方。核心是SOC(荷电状态)的递推公式:
SOC(t) = SOC(t-1) + (eta_ch * P_ch(t) - P_dis(t) / eta_dis) * dt / E_max
其中eta_ch和eta_dis是充放电效率,E_max是储能额定容量。充放电效率说明:充进去1kWh电量,实际能存进电池的是eta_ch倍;放出1kWh电量,电池实际要消耗1/eta_dis倍的内部能量。如果两段效率不统一,继续运行几个时段就会出现SOC“越跑越歪”的现象。
同时还要约束每个时段的SOC都在安全范围内,通常取0.1到0.9之间,避免过充过放。另外必须限制充放电功率不超过额定值。我建议加上充放电互斥约束,虽然理论上储能同时充电和放电既浪费又不符合设备工作原理,但如果不加约束,模型在特定电价条件下真有可能钻空子,利用充放电同时进行来逃避功率平衡约束或套取收益。加互斥约束的常见做法是引入一个二进制变量,充时该变量为1,放时为0,用大M法把充电功率和放电功率分别限制在0或0。
还有一个常被忽略的约束:调度周期结束时SOC应该回到初始值附近。这保证储能调度的“可持续性”——如果今天把电放光了,明天没法继续用。具体做法是约束SOC(最后一个时段) = SOC(初始),或者允许一定范围的偏差。在日前调度中,这相当于强制储能完成一个完整的工作循环。
2.3 风光出力预测与需求响应参数的设定
风光出力在模型里通常处理成不等约束:实际出力不能超过预测出力,但可以低于预测出力(也就是允许弃风弃光)。用预测出力作为上限,让模型自己决定要不要弃电,这样就给调度留出了灵活性。举个实际例子:中午光伏大发时,如果储能已经满电,负荷又不高,此时最优解往往是弃掉一部分光伏,而不是让主网反向倒送电或让储能过充。
需求响应参数设定里面,最关键的可转移负荷约束是“总量守恒”:
sum(P_shift_out(t)) = sum(P_shift_in(t))
这个约束的意思是:转移出去的负荷总量必须等于转移进来的负荷总量,可转移负荷只是改变了用电时刻,并没有减少总用电量。如果漏掉这条,模型就可能“凭空消失”部分负荷,结果算出来的成本低得不真实,但实际根本做不到。
另外还要限制每个时段的转移量上下限,不能让负荷在一个时段内疯狂转移,否则实际电网无法承受。比较常见的设置是单时段最大转移量不超过该时段原负荷的10%~20%,同时设置设备允许转移的时间窗口,比如某类负荷只能在9点到17点之间运行。
可中断负荷的参数相对简单,直接限制单时段削减量上限和全天最大削减次数即可。中断补偿单价通常设得比购电价高一些,否则用户没有动力参与;但会比储能调度的综合成本低一些,这样模型才会优先用储能,再用需求响应,最后才考虑从主网购电。
3. Matlab实现全流程与关键代码拆解
3.1 数据准备与参数初始化
写代码的第一步是定义所有输入数据。我习惯把参数分成三块:系统参数、预测数据、成本参数,这样读代码的人一看就明白每个数的用途。下面给出一个参考结构,用的是24时段调度。
%% 系统参数 dt = 1; % 时段步长,单位小时 T = 24; % 调度时段数 % 储能参数 E_max = 500; % 储能额定容量 kWh P_ch_max = 100; % 最大充电功率 kW P_dis_max = 100; % 最大放电功率 kW eta_ch = 0.95; % 充电效率 eta_dis = 0.95; % 放电效率 SOC_init = 0.2; % SOC初始值 SOC_min = 0.1; % SOC下限 SOC_max = 0.9; % SOC上限 SOC_end = 0.2; % 调度结束SOC目标值 % 需求响应参数 P_shift_max = 30; % 单时段最大可转移负荷量 kW price_trans = 0.6; % 可转移负荷补偿单价 元/kWh price_inter = 0.8; % 可中断负荷补偿单价 元/kWh P_inter_max = 50; % 单时段最大可中断量 kW % 电网交互 P_buy_max = 300; % 最大购电功率 kW P_sell_max = 200; % 最大售电功率 kW % 成本参数 price_buy = [0.8*ones(1,8), 1.2*ones(1,4), 0.9*ones(1,4), 1.4*ones(1,4), 0.7*ones(1,4)]; price_sell = 0.4 * ones(1, T); % 售电价格,通常低于购电价 deg_rate = 0.02; % 储能单位充放电损耗成本 元/kWh penalty = 1.0; % 弃风弃光惩罚系数 元/kWh这里的分时电价向量是我为示例编的,前8小时低谷0.8元,随后两个时段1.2元,午后0.9元,晚高峰1.4元,夜间低谷0.7元。实际用的时候要根据当地的电价政策替换。
需要特别注意电价向量的长度必须和T一致,不然矩阵运算会报错。我调试代码时踩过很多次这种低级坑,所以强烈建议在参数定义之后加一行断言检查:
assert(length(price_buy) == T, '电价向量长度必须等于调度时段数');3.2 Yalmip建模与约束构建
数据准备好了,接下来就是建模。用Yalmip建模的思路非常直白:先用sdpvar定义决策变量,再写目标函数和约束条件,最后调用求解器。决策变量定义如下:
% 决策变量定义 P_pv = sdpvar(1, T); % 光伏实际出力 P_wind = sdpvar(1, T); % 风电实际出力 P_ch = sdpvar(1, T); % 储能充电功率 P_dis = sdpvar(1, T); % 储能放电功率 P_buy = sdpvar(1, T); % 购电功率 P_sell = sdpvar(1, T); % 售电功率 SOC = sdpvar(1, T+1); % SOC,T+1是为了方便递推 P_shift_out = sdpvar(1, T); % 转出负荷 P_shift_in = sdpvar(1, T); % 转入负荷 P_inter = sdpvar(1, T); % 可中断负荷削减量 P_curtail = sdpvar(1, T); % 弃风弃光总量 u_ch = binvar(1, T); % 充电状态0-1变量 u_dis = binvar(1, T); % 放电状态0-1变量注意SOC定义成T+1个变量,索引1对应的初始值已知,索引2到T+1对应第1到第T个时段末的状态。这样递推时用SOC(t) = SOC(t-1) + ...的写法非常自然,避免数组索引错位的问题。
约束条件按组添加:
Constraints = []; % 功率平衡约束 for t = 1:T Constraints = [Constraints, P_pv(t) + P_wind(t) + P_dis(t) + P_buy(t) == ... P_load(t) + P_ch(t) + P_sell(t) + P_shift_in(t) + P_inter(t)]; end % 储能SOC递推 Constraints = [Constraints, SOC(1) == SOC_init]; for t = 1:T Constraints = [Constraints, SOC(t+1) == SOC(t) + (eta_ch * P_ch(t) - P_dis(t)/eta_dis) * dt / E_max]; Constraints = [Constraints, SOC_min <= SOC(t+1) <= SOC_max]; Constraints = [Constraints, 0 <= P_ch(t) <= P_ch_max * u_ch(t)]; Constraints = [Constraints, 0 <= P_dis(t) <= P_dis_max * u_dis(t)]; Constraints = [Constraints, u_ch(t) + u_dis(t) <= 1]; end Constraints = [Constraints, SOC(T+1) == SOC_end]; % 风光出力与弃电约束 for t = 1:T Constraints = [Constraints, 0 <= P_pv(t) <= P_pv_pred(t)]; Constraints = [Constraints, 0 <= P_wind(t) <= P_wind_pred(t)]; Constraints = [Constraints, P_pv(t) + P_wind(t) + P_curtail(t) == P_pv_pred(t) + P_wind_pred(t)]; end % 需求响应约束 Constraints = [Constraints, sum(P_shift_out) == sum(P_shift_in)]; for t = 1:T Constraints = [Constraints, 0 <= P_shift_out(t) <= P_shift_max]; Constraints = [Constraints, 0 <= P_shift_in(t) <= P_shift_max]; Constraints = [Constraints, 0 <= P_inter(t) <= P_inter_max]; end % 电网交互约束 for t = 1:T Constraints = [Constraints, 0 <= P_buy(t) <= P_buy_max]; Constraints = [Constraints, 0 <= P_sell(t) <= P_sell_max]; end这里讲几个关键的建模细节。
第一,充放电互斥约束我用了两个0-1变量u_ch和u_dis,并通过u_ch(t) + u_dis(t) <= 1保证不会同时充放。0-1变量直接参与了功率变量上限的约束,这种方式叫做大M法的特殊形式——把功率上限乘以0-1状态变量,当状态为0时功率必须为0,状态为1时功率上限是额定值。
第二,SOC初值必须等于SOC_init,这是递推的起点。调度结束时约束SOC(T+1) == SOC_end,让储能回到初始状态,形成完整循环。有些代码只约束初值不约束终值,那末调度结果会出现储能“趁机偷懒”的问题,比如最后一个时段把电全部放光,虽然降低了当天成本,但第二天就没法正常运作了。
第三,需求响应的总量守恒约束sum(P_shift_out) == sum(P_shift_in)是整个模型中最容易出现不可行问题的约束之一。模型不可行通常表现为求解器报“Infeasible problem”或者“No feasible solution found”。原因多半是负荷总量太小,转移窗口有限,无法满足总量守恒。后续在第4节排错部分会详细展开。
目标函数用如下方式定义:
objective = sum(price_buy .* P_buy * dt) - sum(price_sell .* P_sell * dt) ... + deg_rate * sum(P_ch + P_dis) * dt ... + price_trans * sum(P_shift_out) * dt ... + price_inter * sum(P_inter) * dt ... + penalty * sum(P_curtail) * dt; optimize(Constraints, objective, sdpsettings('solver', 'cplex', 'verbose', 1));Yalmip会自动识别问题类型,因为模型中有0-1变量,它会以MILP的形式交给求解器处理。这里面的核心逻辑是:目标函数每一项都是决策变量的线性组合,所以整个目标也是线性的。只要约束条件保持线性,模型就是一个标准的MILP。
3.3 求解结果处理与可视化
求解完之后,用value命令把所有变量的数值取出来。我发现不少新手会忘记在优化结束后执行value提取,直接操作sdpvar对象,导致后续的数据绘制全是错的。这一点务必注意。
P_pv_opt = value(P_pv); P_wind_opt = value(P_wind); P_ch_opt = value(P_ch); P_dis_opt = value(P_dis); P_buy_opt = value(P_buy); P_sell_opt = value(P_sell); SOC_opt = value(SOC); P_shift_out_opt = value(P_shift_out); P_shift_in_opt = value(P_shift_in); P_inter_opt = value(P_inter); P_curtail_opt = value(P_curtail);还可以顺手算一下总成本,作为经济性指标:
total_cost = value(objective); fprintf('调度总成本:%.2f 元\n', total_cost);经典的绘图输出包括:电力平衡堆叠图、储能SOC变化曲线、需求响应前后的负荷曲线对比图。下面给一个基础的绘画代码,绘制功率平衡和SOC:
figure; t = 1:T; subplot(2,1,1); bar(t, [P_pv_opt; P_wind_opt; P_dis_opt; P_buy_opt]', 'stacked'); hold on; plot(t, P_load + P_shift_in_opt + P_inter_opt, 'r-o', 'LineWidth', 1.5); plot(t, P_load, 'k--', 'LineWidth', 1.2); legend('光伏出力','风电出力','储能放电','购电功率','调整后负荷','原始负荷'); xlabel('时段/h'); ylabel('功率/kW'); title('功率平衡与负荷调整'); subplot(2,1,2); plot(t, SOC_opt(2:end), 'b-o', 'LineWidth', 1.5); xlabel('时段/h'); ylabel('SOC'); title('储能荷电状态变化'); grid on;这个结果图基本可以支撑对调度方案的分析。从堆叠图中可以看到哪些时段光伏和风电大发、哪些时段需要储能放电或者从大电网购电;调整后负荷曲线与原始负荷曲线的对比则能直观看出需求响应的削峰填谷效果。
3.4 求解器安装与报错排查
用Yalmip时经常碰到的问题是求解器没装好或者路径没配好。Yalmip本身只是一个建模层,它不求解,需要调用后端求解器。装Cplex或Gurobi时,要把求解器的bin文件夹路径加入Matlab的路径:
addpath('C:\Program Files\IBM\ILOG\CPLEX_Studio128\cplex\matlab\x64_win64'); savepath;装完之后在Matlab里运行yalmip('clear')再重新optimize,确保Yalmip重新加载求解器列表。如果求解器没有正确加载,Yalmip会报“No suitable solver found”之类的错误,那说明Yalmip根本没检测到Cplex或Gurobi。
如果不想装商业求解器,也可以直接使用intlinprog作为求解器。Yalmip支持设置solver为'intlinprog',Matlab自带的优化工具箱就能跑MILP,只是求解速度和稳定性稍逊色一些。对于24时段的微电网调度来说,intlinprog通常够用。
4. 常见问题与排查技巧实录
4.1 模型无解:不是代码错,是约束过紧
调度模型最常见的问题是求解器报infeasible。很多新手第一反应是代码写错了,但根据我的经验,代码本身往往没问题,问题出在约束条件过强。最典型的场景就是需求响应的总量守恒约束加进去之后,模型立刻无解。
为什么会出现这种情况?因为如果可转移负荷的比例设得太大,但负荷曲线本身的波动又不够大,模型找不到足够的低谷时段来安置转移过来的负荷。比如午夜时段负荷本身就只有50kW,但你允许单时段转移量最大60kW,这就意味着最多只能有50kW负荷能转进来,没法满足总量守恒,模型直接无解。
排查方法也很直接:把总量守恒约束注释掉,看模型是否还能求解。如果能solve,说明问题就出在这个约束上。然后逐步收紧允许转入的负荷上限,比如把P_shift_max从60降到40、30,直到找到可行域。这里体现出的规律是:可转移负荷的最大转移量不是越大越好,它受到实际负荷曲线形状的约束。
另外也可能是因为奖惩系数矛盾导致目标无界或者约束冲突。比如可中断负荷补偿单价设得比购电价高出很多,而负荷削减量又设得很大,模型可能倾向于尽量削减负荷来降低成本,但负荷削减量过多后,功率平衡约束难以满足,解空间被挤压。
我建议在调试时候加入一个可行性诊断脚本,逐个打印各约束的松弛量:
check(Constraints);Yalmip的check命令会显示每个约束的残差,非常高效。哪一行残差特别大,哪一行基本就是问题所在。
4.2 储能SOC越界:效率参数和递推逻辑的坑
SOC越界一般表现在求解结果里SOC曲线超出0到1范围,或者在某个时段突变到负值。这种问题的根源几乎都在递推公式。
最常见的低级错误是dt和E_max的单位不匹配。比如储能容量是500kWh,但调度步长是15分钟,dt=0.25小时,如果代码里写成dt=1,那每次递推的SOC变化量会被夸大4倍,SOC很容易冲出上限。这种问题核对了代码往往一眼就能发现,但确实容易踩。
另外一个常见问题是充放电效率的放置位置。正确的公式是:
SOC(t+1) = SOC(t) + (eta_ch * P_ch - P_dis / eta_dis) * dt / E_max
有些代码把放电效率写成了乘以效率而不是除以效率,这会导致放电时SOC下降过快,而且整个循环里储能的“有效吞吐量”对不上。检查效率公式有没有写反,最直接的办法是做一个手动推演:让储能从SOC=0.5开始,以P_ch=100充电1小时,如果eta_ch=0.95,E_max=500,充完之后SOC应该等于0.5+0.951001/500=0.69。如果代码算出来的不一样,那就是公式的问题。
还有一种情况是SOC初始值没有赋值。Yalmip的sdpvar默认值不是0,而是符号变量,如果不约束SOC(1) == SOC_init直接参与递推,结果会非常荒诞。所以SOC初值约束必须放在递推之前。
4.3 求解结果出现同时充放电
如果没加互斥约束,或者加了但大M系数选得不合适,可能出现同一时段充放电功率都为正的结果。很多情况下求解器给出的最优解表面上看起来充放同时进行,实际上去核对目标函数值会发现,这其实是模型在利用充放电同时发生来“制造”损耗——这在某些设置下会让目标函数变小,但物理上是荒唐的。
解决方法是严格加上互斥约束。如果担心0-1变量计算量扩大,还有个折中办法:给同时充放电额外加惩罚,比如在目标函数里加一个大系数的P_ch+P_dis项,人为抑制同时充放行为。但这个方法只能算补救,最彻底的做法还是用二进制变量做硬约束。
4.4 需求响应参数怎么定才合理
需求响应参数设不好,调度结果容易出现两种极端:要么需求响应参与量极小,形同虚设;要么负荷曲线被“削”得过分平整,实际没法执行。
先说参与量极小的情况。如果补偿单价远低于低谷电价与高峰电价差,模型计算后会发现“把负荷从高峰挪到低谷”省下的电费还抵不上要支付给用户的补偿费,自然就不愿意调用需求响应。以我的经验,可转移负荷补偿单价应该设置在峰谷价差的50%~70%之间,才有合理的调用频率。比如峰谷价差0.6元/kWh,补偿单价定在0.3到0.4元/kWh比较合适。
再说削得太狠的情况。如果单时段允许的转移量特别大,模型很可能会把某个高峰时段的全部负荷都平移到低谷时段,造成新的“次高峰”。实际操作中单时段可转移量最好控制在原始负荷的10%~20%以内。同时可转移负荷总量也需要控制在日总负荷的一定比例内,比如不超过总用电量的10%。这样需求响应才符合物理可行的调整能力,调度结果也才具备指导意义。
4.5 求解速度过慢:问题规模控制
当调度时段为96点甚至更多时,MILP的0-1变量数量会剧增导致求解时间过长。有时候跑一个调度要等几十分钟甚至几小时,这种体验非常痛苦。
可以从几个方向压缩计算量:
一是减少不必要的0-1变量。比如储能互斥约束可以用互补约束近似处理,或者通过目标函数中设惩罚项间接避免同时充放,省掉2T个0-1变量。
二是缩小决策变量的取值范围。比如将P_buy的搜索上界设为实际最大购电功率,避免求解器在宽松范围内盲目搜索。
三是调整求解器的容差参数。在sdpsettings中设置cplex的MIP gap,比如将相对gap设为0.01(1%),让求解器在解足够接近最优解时就提前终止,能大幅度缩短求解时间。对工程应用来说,1%的gap远够用了。
最后再分享几个实际调试中的小技巧
我做这类调度代码调试时,习惯先跑一个简化版的模型——把需求响应去掉,只保留储能和购电,先确认功率平衡和储能递推逻辑没问题;然后逐步加入可转移负荷、可中断负荷,每加一个模块就重新验算一次可行性。这样出问题时能快速锁定是哪个新加的约束引起的,而不是面对一整套复杂模型无从下手。
另一个实用习惯是把每一步运算的物理单位在心里过一遍。功率单位是kW,乘上时间单位h得到电量单位kWh,再除以储能容量单位kWh,得到的才是无量纲的SOC变化量。很多奇怪的报错,归根到底都是单位不统一闹的。
这个项目后续还可以继续扩展:比如把确定性日前调度升级为多场景随机优化,处理风光出力的不确定性;或者引入实时滚动修正,形成日前+日内两级调度框架;再或者把电动汽车充放电也纳入需求响应资源,做成车网互动(V2G)的模式。这些都是在现在这个模型框架上自然生长出来的方向,代码基础打好了,后面加东西会顺畅很多。