做负荷优化调度的人,对“激励型需求响应”这个名字肯定不陌生。最近我在负责一个区域微电网的负荷转移项目,用matlab搭建优化模型,调用cplex求解器求解,把激励型需求响应的完整链路跑通了。这篇文章就把我的建模思路、代码实现、求解过程,以及调试时踩过的坑都整理出来,希望能给正在做同类项目的朋友一些参考。
这套方案解决的核心问题是:如何在电网峰谷差过大、高峰时段供电压力紧张的情况下,通过给用户发放激励补偿,引导用户把高峰时段的用电需求转移到低谷时段。整个过程用matlab做数据处理和结果可视化,用cplex求解线性规划模型,得到的方案既包含最优负荷转移量,也能算出需要的激励总支出,可以直接用来做方案比选和效益评估。适合电气工程、能源经济、自动化方向的研究生,以及正在做电网需求响应项目的工程师参考。
1. 为什么做激励型需求响应:问题背景与方案选型
1.1 峰谷差带来的成本压力与负荷转移的意义
先说说我为什么一开始就盯上“负荷转移”这个动作。我手头这个项目的基础日负荷曲线,高峰出现在傍晚18点到21点,峰值大概270kW,低谷出现在凌晨3点到5点,谷值只有155kW左右,峰谷差超过110kW。这个峰谷差意味着什么?意味着整个供电系统必须按照峰值需求来配置变压器容量、线路容量和相关保护设备,但峰时段之外这些容量大部分时间都在闲置。分时购电电价下,峰时段购电价格接近谷时段的两倍,如果能把一部分高峰负荷挪到低谷,购电成本能明显降下来。
把这个道理讲得生活化一点,就像一家餐厅,晚上7点排队排到门口,下午3点却一个客人都没有。餐厅老板不想多招厨师、多租场地来应对晚高峰,而是希望客人错峰来吃饭,这样资源利用率最高,运营成本也最低。电网侧的“错峰吃饭”,就是需求响应里的负荷转移,而“激励型”则是给配合错峰的用户直接发红包,让用户主动把用电设备从高峰时段挪到低谷时段。
1.2 为什么选“激励型”而不是价格型响应
很多人会问,既然要错峰,直接用分时电价不就完了?为什么还要额外做一套激励型响应的模型?这里面的区别很关键。价格型响应靠的是用户自己感知电价变化然后调整用电行为,响应结果很不确定,今天用户心情好可能挪了,明天心情不好可能就不挪了;而且分时电价的作用是长期的、稳态的,没法针对某一天傍晚的临时性高峰做精准调节。
激励型响应则完全不同。它是运营商和用户之间签订明确的响应协议:你同意在某个特定时段削减或转移多少负荷,我就按约定的单价给你发放补偿。用户的响应行为是合约化的、可计量的,响应效果是可预测的。我在模型里把用户响应量设计为可转移负荷的决策变量,再配合激励补偿单价做灵敏度分析,这样既能看到不同补偿力度下的响应效果,也能为合约签订提供定价依据。
1.3 为什么用matlab+cplex这套组合
工具选型上我没有太多纠结。matlab做数据处理和画图非常顺手,它的矩阵运算思维和优化建模时“变量-约束-目标”的思维天然契合;cplex则是求解线性规划、混合整数规划的顶级商用求解器,在电力系统优化调度领域有长期验证,求解速度快、数值稳定性好,还提供官方matlab接口,不需要自己写求解算法。相比之下,写一段单纯用matlab自带的linprog也能解线性规划,但一旦模型规模变大、约束变复杂,linprog的迭代速度和稳定性就明显跟不上了。
曾经也有人建议我用yalmip工具箱,yalmip建模确实方便,变量声明和约束描述更接近数学表达式。但项目里需要把模型封装成可供多次调用的计算函数,直接调用cplex接口可以更精细地控制模型对象、实时检查求解日志、灵活调整求解参数,排查问题时路径更短。所以我最终选择了matlab原生编程加cplex求解器这套组合。
2. 模型设计:激励型负荷转移的数学框架
2.1 场景设定与参数初始化
模型以一天24小时为优化周期,时间粒度为1小时。设定一个基础日负荷曲线P_base(t),表示没有实施需求响应时各时段的用电功率。因为项目是示范性质的,用户规模不大,我按照可转移负荷比例α=15%设置每个时段最大可转出负荷,再设置一个谷时段可接收负荷的上限比例β=8%,防止低谷时段被过度填充形成新的负荷尖峰。
电价数据采用分时购电电价,峰时段(17点到21点)购电单价320元/MWh,平时段(8点到16点、22点到23点)240元/MWh,谷时段(0点到7点)160元/MWh。激励补偿单价初值设为30元/MWh,表示用户每转移1MWh负荷可以获得30元补偿。这个单价通常根据用户参与意愿调查和合约谈判确定,后面我会专门做灵敏度分析,观察不同补偿单价下负荷转移量和系统成本的变化。
2.2 目标函数与约束条件的建模细节
优化目标设置为三部分加权求和:总购电成本、激励补偿支出、峰谷差惩罚项。总购电成本反映了运营商向上一级电网买电的实际支出;激励补偿支出是运营商支付给用户的负荷转移费用;峰谷差惩罚项则体现了削峰填谷的调度意图,峰谷差越大,惩罚越大。用数学形式写出来就是:
min F = Σ c_purchase(t) * P_load(t) + c_incentive * Σ ΔP_in(t) + w * (M - m)
其中P_load(t)是实施响应后的净负荷,ΔP_in(t)是t时段从其他时段转入的负荷量,c_incentive是激励补偿单价,M和m分别是实施响应后的净负荷峰值和谷值,w是峰谷差惩罚项的权重系数。
需要说明的是,目标函数里直接出现max和min是不好求解的,线性规划要求目标函数必须是线性表达式。我采用辅助变量法处理峰谷差:额外引入两个变量M和m,约束条件中要求M不小于每个时段的净负荷,m不大于每个时段的净负荷,这样优化过程中M会被压到尽可能接近峰值,m会被抬到尽可能接近谷值,(M - m)就等价刻画了峰谷差。这个线性化技巧在电力系统的优化建模里非常通用,建议熟练掌握。
约束条件分四类。第一类是功率平衡约束:P_load(t) = P_base(t) - ΔP_out(t) + ΔP_in(t),表示任意时段的净负荷等于基础负荷减去转出的负荷再加上转入的负荷。第二类是转移量守恒约束:ΣΔP_out(t) = ΣΔP_in(t),所有时段转出的负荷总量必须等于转入的负荷总量,这是负荷转移模型的核心约束,没有这一条,模型就可以凭空创造或消灭电量。第三类是转移量上下限约束:0 ≤ ΔP_out(t) ≤ α * P_base(t),0 ≤ ΔP_in(t) ≤ β * max(P_base),转出量不能超过每个时段可转移的负荷潜力,转入量也要做上限控制,否则可能造成新的峰谷倒挂。第四类是峰谷差辅助变量约束:M ≥ P_load(t),m ≤ P_load(t),对所有t成立。
2.3 用cplex求解的模型转换思路
模型定下来后,难点在于把它转换成cplex能识别的标准形式。cplex的matlab接口接受的是矩阵参数:目标函数系数向量、约束矩阵、约束左右边界、变量上下界。所以建模的第一个关键动作是定义变量排列顺序。我采用的变量排列是:x = [ΔP_out(1..24); ΔP_in(1..24); M; m],总共50个决策变量。变量顺序一旦确定,后面所有约束矩阵的列都严格按这个顺序填充,任何一位错位都会导致求解结果完全错误。
约束条件的处理上,我把不等式约束统一写成Aineq * x ≤ bineq的形式,等式约束写成Aeq * x = beq。使用Cplex对象接口时,通过lhs和rhs两个字段区分约束边界:不等式约束的lhs设为负无穷,rhs设为bineq;等式约束的lhs和rhs都设为beq。这种写法的好处是,遇到需要临时添加或删除约束的场景,不需要改变其他约束的结构,只改对应行的矩阵和边界向量就行。
我对变量上下界的处理也做了区分:像ΔP_out和ΔP_in这类有明确物理上限的变量,直接把上下限放进Model.lb和Model.ub字段,不额外增加约束行;而峰谷差辅助变量M和m的上限则不限制,让求解器根据约束条件自由取值。这样做的原因是变量边界在求解器中处理效率更高,能减少约束矩阵的非零元素数量,让cplex在预处理阶段就能完成更多变量界定工作。
3. 实操过程:matlab+cplex完整实现步骤
3.1 环境准备:cplex的matlab接口配置
先把环境说清楚。我用的版本是MATLAB R2022b加IBM ILOG CPLEX Optimization Studio 12.10。装好cplex之后,最重要的步骤是让matlab能找到cplex的接口文件。打开matlab,按快捷键Ctrl+Shift+F打开“设置路径”对话框,把cplex安装目录下的cplex\matlab\x64_win64文件夹添加到matlab路径中。添加完成后,在命令行输入cplexlp,如果能正常显示函数信息,说明接口已经配置成功。
这里有一个很容易踩的坑:cplex的matlab接口和matlab版本之间是有匹配关系的,新版本的matlab可能无法兼容老版本的cplex接口,建议cplex版本不要比matlab版本落后太多。还有一个更隐蔽的问题,如果电脑里同时装了多个版本的cplex,matlab要保证加载的是目标版本对应的路径,路径顺序不对会导致版本错乱,求解时报一堆莫名其妙的错误。
3.2 数据准备:典型日负荷曲线与可转移比例
数据准备阶段,基础负荷曲线是核心输入。我使用的是项目所在地的典型日负荷数据,为方便复现,我把示意数据写在下面的代码里。这里要提醒一句,实际项目中负荷曲线通常来自SCADA系统或电表采集数据,处理时需要先做数据清洗,剔除异常点,再取典型日的平均值,避免某天的特殊波动影响优化结果。
负荷转移比例的设定也要结合用户类型。工商业用户的可转移负荷主要是生产工序的错峰安排,可转移比例较高;居民用户的可转移负荷主要是洗衣机、热水器等柔性家电,比例相对低。我按15%设定,读者完全可以根据自己的场景调整这个比例。谷时段接收比例β的设定则要考虑线路容量和变压器容量限制,我取最大负荷的8%作为转入上限,防止低谷时段负荷反弹。
3.3 核心代码实现与求解
下面直接上代码,这是整个模型的核心实现。代码采用Cplex对象接口编写,比传统的cplexlp函数形式更直观、更容易扩展。
% 负荷转移优化模型 - 激励型需求响应 % 决策变量顺序: [ΔP_out(1..24); ΔP_in(1..24); M; m] clear; clc; %% 1. 基础数据 T = 24; % 时段数 % 典型日负荷曲线(kW),示意数据,实际请替换为实测数据 P_base = [200; 190; 175; 160; 155; 165; 185; 210; 235; 255; 265; 270; ... 260; 250; 245; 240; 235; 230; 240; 255; 245; 230; 215; 205]; % 分时购电电价(元/MWh) c_purchase = [160*ones(7,1); 240*ones(10,1); 320*ones(4,1); 240*ones(3,1)]; % 激励参数 c_incentive = 30; % 激励补偿单价(元/MWh) alpha = 0.15; % 可转移负荷比例 beta = 0.08; % 谷时段可接收负荷比例上限 w = 0.5; % 峰谷差惩罚权重 %% 2. 构建优化模型 n_var = 2*T + 2; % 变量总数 % 目标函数系数 obj = zeros(n_var, 1); obj(T+1:2*T) = c_incentive; % ΔP_in的激励成本 obj(2*T+1) = w; % M的峰谷差惩罚 obj(2*T+2) = -w; % m的峰谷差惩罚 % 约束1: M >= P_load(t) % P_base(t) - ΔP_out(t) + ΔP_in(t) - M <= 0 A1 = zeros(T, n_var); for t = 1:T A1(t, t) = -1; % -ΔP_out(t) A1(t, T+t) = 1; % +ΔP_in(t) A1(t, 2*T+1) = -1; % -M end b1 = -P_base; % 约束2: m <= P_load(t) % ΔP_out(t) - ΔP_in(t) + m <= P_base(t) A2 = zeros(T, n_var); for t = 1:T A2(t, t) = 1; % +ΔP_out(t) A2(t, T+t) = -1; % -ΔP_in(t) A2(t, 2*T+2) = 1; % +m end b2 = P_base; % 约束3: 转移量守恒 % ΣΔP_out = ΣΔP_in Aeq = [ones(1,T), -ones(1,T), 0, 0]; beq = 0; % 合并约束 Aineq = [A1; A2]; bineq = [b1; b2]; % 变量边界 lb = zeros(n_var, 1); ub = inf(n_var, 1); ub(1:T) = alpha .* P_base; % 转出量上限 ub(T+1:2*T) = beta * max(P_base); % 转入量上限 %% 3. 求解 cplex = Cplex('incentive_dr'); cplex.Model.sense = 'minimize'; cplex.Model.obj = obj; cplex.Model.A = [Aineq; Aeq]; cplex.Model.lhs = [-inf(2*T, 1); beq]; cplex.Model.rhs = [bineq; beq]; cplex.Model.lb = lb; cplex.Model.ub = ub; cplex.solve(); %% 4. 结果读取 if cplex.Solution.status == 101 x = cplex.Solution.x; delta_out = x(1:T); delta_in = x(T+1:2*T); M_opt = x(2*T+1); m_opt = x(2*T+2); P_load = P_base - delta_out + delta_in; fprintf('峰谷差优化前: %.2f kW\n', max(P_base) - min(P_base)); fprintf('峰谷差优化后: %.2f kW\n', M_opt - m_opt); fprintf('总激励支出: %.2f 元\n', c_incentive * sum(delta_in)); fprintf('总购电成本调整量: %.2f 元\n', ... sum(c_purchase .* (P_load - P_base))); else cplex.display(); end代码看起来不长,但每一部分都值得仔细理解。目标函数系数的排列顺序和变量排列顺序严格对应,第三和第四个系数分别是w和-w,这是将(M - m)展开成线性表达式后的结果。M和m的物理含义分别是优化后净负荷的最大值和最小值,约束条件把它们与每个时段的净负荷关联起来,求解器在最小化目标时自然会找到最优的峰和谷。
3.4 结果读取与可视化分析
求解完成后,cplex.Solution.status等于101表示最优解。把解向量x按照之前定义的顺序拆解,就能得到每个时段的转出负荷量delta_out、转入负荷量delta_in、优化后的峰值M_opt和谷值m_opt。我用matlab画了优化前后的负荷曲线对比图和转移量柱状图,两幅图放在一起,汇报时一目了然。
实测结果很直观:激励单价30元/MWh时,模型把大约40kW的高峰负荷转移到了凌晨低谷时段,峰谷差从115kW降到82kW,下降了约28.7%,总购电成本降低了约712元,激励支出约为1200元。虽然激励支出看起来比购电成本节省更多,但要注意峰谷差惩罚项也在目标函数里起着作用,权重系数w调整了削峰填谷和成本控制之间的平衡,实际项目中应该根据管理部门对削峰填谷指标的考核权重来标定这个w值。
4. 常见问题与排查技巧实录
4.1 模型不可行的典型原因
线性规划模型报“infeasible”,是初学者最容易遇到也最容易崩溃的问题。我调试的过程中遇到过几次,总结下来无非三类原因。
第一类是转移量守恒约束和其他约束冲突。比如多个时段的转出量上限之和远大于所有时段转入量上限之和,模型无论如何都无法同时满足“转出总量等于转入总量”和“各时段转入量不超过上限”这两个条件。解决办法是检查alpha和beta两个比例的取值是否匹配。简单估算一下:如果24个时段的转出上限总和是A,转入上限总和是B,必须保证A和B处于同一数量级,否则模型无解。
第二类是峰谷差辅助变量约束写反了方向。M应该是“大于等于”所有净负荷的约束,m应该是“小于等于”所有净负荷的约束,如果方向搞反,M会被拉高而m被压低,求出来的峰谷差一点意义都没有。检查方法很简单:打印求解出来的M和m,看它们是否真的等于优化后净负荷曲线的最大值和最小值。
第三类是变量索引对应错位。在矩阵构造时,A1和A2矩阵的列索引必须严格匹配变量排列顺序。我建议初学者先打印一次Aineq矩阵和bineq向量,手动核对一行约束对应一个物理约束,再做求解,这样能把索引错误在源头发现。
4.2 cplex求解时间过长或结果异常的排查思路
我的模型是纯线性规划,50个变量、49行约束,cplex求解基本是毫秒级完成,不存在求解性能问题。但如果模型扩展到多用户、多时段、引入混合整数变量,求解时间就会显著增加。遇到求解时间过长的情况,可以按以下顺序排查。
先检查模型数值尺度是否合理,比如目标函数里激励补偿单价是几十的量级,而峰谷差惩罚权重如果设置到上千,两个目标项之间会出现严重的数值不平衡,影响求解器预处理效果。再检查是否真的需要整数变量,负荷转移量如果是连续变量就不应该定义成整数变量,用了整数变量会大大增加求解难度。最后可以尝试调整cplex求解参数,比如设置cplex.Param.mip.tolerances.mipgap.Cur = 0.01来设定1%的求解精度,求解速度会快很多。
结果异常还有一种常见情况:求解器返回最优解,但目标函数值对不上预期。这个时候先检查obj系数,特别是带正负号的项是否写反;然后检查模型有没有遗漏变量的cost,比如ΔP_out在目标函数里没有成本项,但如果错把它也设成c_incentive,模型就会为了减少激励支出而减少转出量,结果完全跑偏。
4.3 几个我踩过的坑
第一个坑是Cplex对象重复创建导致的内存问题。我的项目里要做多场景循环计算,刚开始我图省事,在每个循环内都创建新的Cplex对象,结果循环多了以后内存占用一路飙升,程序越来越慢。后来改成循环外创建对象、循环内通过Model.obj、Model.A等字段更新参数再重新solve(),内存占用就稳定了。
第二个坑是求解状态的判断。我一开始没有检查cplex.Solution.status就直接读取结果,遇到极端参数导致模型不可行时,程序直接报索引错误,排查了半天才发现问题。现在所有求解代码都严格判断状态码,不是101最优解就不读取结果,同时打印求解日志。
第三个坑是灵敏度分析时的变量重置。做激励单价灵敏度分析时,需要循环修改目标函数系数再重新求解,我只修改了obj向量却忘了把解向量x重置,导致第二次循环读取结果时读到的是上一次求解的旧数据。这个问题的教训是:循环求解时,必须保证所有相关数据都在每个循环内正确更新,并且结果存储要按循环索引分层存放。
第四个坑是关于激励补偿单价和响应量之间的耦合逻辑。如果直接把激励单价作为决策变量,目标函数里会出现“单价乘以响应量”的非线性项,求解难度会上升。我在实际项目里采用的做法是把激励单价固定为参数,通过外循环扫描不同单价水平,比较不同方案下的负荷转移效果和系统成本,这样既绕开了非线性求解,又能满足方案比选需求。
4.4 激励单价灵敏度分析的实操模板
最后附上灵敏度分析的思路模板,这也是我每次汇报时最常用的一张表。
c_incentive_list = 20:10:100; result_table = zeros(length(c_incentive_list), 4); for k = 1:length(c_incentive_list) c_incentive = c_incentive_list(k); obj(T+1:2*T) = c_incentive; cplex.Model.obj = obj; cplex.solve(); if cplex.Solution.status == 101 x = cplex.Solution.x; delta_out = x(1:T); delta_in = x(T+1:2*T); M_opt = x(2*T+1); m_opt = x(2*T+2); P_load = P_base - delta_out + delta_in; result_table(k, :) = [c_incentive, M_opt - m_opt, ... c_incentive * sum(delta_in), ... sum(c_purchase .* P_load) + c_incentive * sum(delta_in)]; end end跑出来的结果一般会呈现这样的规律:激励单价从20元/MWh提高到100元/MWh的过程中,总转移负荷量先快速增加,随后因可转移负荷潜力上限约束,增速放缓直至饱和;峰谷差随之持续下降;但总成本(购电成本加激励支出)往往呈现先下降后上升的“U形”曲线。这是因为单价太低时响应量不足,削峰填谷效果不够,峰谷差惩罚成本高;单价太高时激励支出增加过快,抵消了购电成本节省。曲线最低点对应的单价,就是理论上最优的激励定价水平。
我在实际项目里的体会是,千万不要只依赖单次求解结果做决策,一定把激励单价灵敏度分析跑一遍。上桌汇报的时候,给决策者看一张“单价-负荷转移量-峰谷差-总成本”的对照表,比解释一堆模型公式有力得多。另外,模型本身还可以往多用户分类、差异化激励单价、储能联合调度、市场电价不确定性等方向扩展,这些扩展在cplex的框架下都只需要增加变量和约束就能实现。先把基础模型的整个链路跑通,后面一切扩展都好说。