
做需求响应仿真的这两年我身边不少同行还在用Excel表格硬算激励费用算完还要手动截图拼曲线效率低不说模型稍微改个约束条件就得从头捋一遍。直到我把激励型负荷需求响应模型完整搬到Matlab里用优化工具箱加脚本化数据处理才真正体会到什么叫“改参数就能换场景”。这篇文章就把我踩过的坑、走过的弯路、最终成型的一套可复现代码框架原原本本分享出来。先说清楚一件事这篇文章适合谁。如果你正在做电力系统需求响应方向的课题或者工作中需要评估激励型需求响应方案的削峰效果、激励成本再或者你脑子里已经有了模型公式但不知道怎么用Matlab落地那这篇文章就是给你准备的。项目本身的定位是把“激励型负荷需求响应模型”从数学公式变成可运行、可出图、可调参的Matlab仿真程序核心产出包括用户基线负荷生成、激励报价排序、削峰目标分配、多时段优化调度以及结果可视化。1. 模型设计思路为什么选激励型而不是价格型1.1 激励型需求响应的核心逻辑需求响应按触发方式分成两大类价格型Price-based DR和激励型Incentive-based DR。价格型的逻辑是电价波动引导用户自发调整用电激励型则直接得多——电力公司与用户签订协议到了用电高峰或者系统需要应急调节的时刻电力公司提前通知用户削减负荷削减多少、持续多久都提前约定好事后按削减量给予货币补偿。激励型的优势在于可控性强。价格型遇到尖峰电价用户可能不理睬但激励型一旦签了约就有合同约束削减量按协议执行运营商可以精确算出来总共能削减多少这对电网稳定运行至关重要。我们做的模型就从“运营商视角”出发已知预测负荷曲线和削峰目标已知一批已签约用户的基本信息容量上限、申报激励单价、最大参与次数要在满足削峰目标的前提下把总激励成本压到最低。1.2 模型假设与适用范围在动手写代码前先把模型边界框定清楚否则后面会越来越乱。我这套Matlab实现基于以下假设条件每个用户有独立的基线负荷曲线可削减量不超过该时刻基线负荷的某个比例一般不超过30%用户申报的激励单价是固定的不随削减量变化现实中这个单价可以是市场出清价格或者合同价格系统采用集中式调度用户不隐瞒申报信息不存在博弈行为调度周期按小时离散化采用滚动优化的方式逐时求解。这些假设如果没有被满足模型就需要额外扩展。比如用户报价动态变化就需要引入博弈论模型再比如削减量超过基线负荷的一定比例会造成用户满意度大幅下降就需要增加舒适度约束。但作为第一版可运行的模型上面的假设足够支撑一个结构清晰的仿真。2. 数学建模把文字问题变成优化问题2.1 决策变量与目标函数设共有 N 个已签约用户调度时段编号 t 1,2,...,T。定义决策变量(P_{i,t}^{cut})用户 i 在时段 t 的实际削减功率单位kW连续变量(s_{i,t})用户 i 在时段 t 是否参与响应二进制变量参与为1否则为0。目标函数是所有时段所有用户激励补偿费用之和最小化[ \min \sum_{t1}^{T} \sum_{i1}^{N} price_i \times P_{i,t}^{cut} ]price_i 是用户 i 申报的单位激励单价单位元/kWh或者元/kW。注意如果模型按小时离散化且削减功率单位是kW那么一个小时的削减电量就是 P_i,t^cut 乘以 1小时所以 price_i 的量纲需要和电费单价的习惯保持一致通常我们直接用元/kW·h来处理这样目标函数里的乘积就能对应实际支付的激励费用。2.2 约束条件四类约束缺一不可约束条件我分成四类这也是我调试过程中最容易出问题的地方。第一类是削峰目标约束。每个时段所有用户削减量的总和要达到系统下发的期望削减量 D_t[ \sum_{i1}^{N} P_{i,t}^{cut} D_t ]如果考虑系统允许一定偏差可以把等式改成不等式 (\ge D_t)具体看场景。第二类是用户削减能力约束。每个用户每个时段的削减量有两个限制一个是最小启动削减阈值 (P_{i}^{min})低于这个值用户不愿意配合一个是最大可削减量 (P_{i}^{max})[ P_{i}^{min} \times s_{i,t} \le P_{i,t}^{cut} \le P_{i}^{max} \times s_{i,t} ]第三类是最大参与时长约束。每个用户在整个调度周期内参与削减的总时段数不能超过 K_i[ \sum_{t1}^{T} s_{i,t} \le K_i ]第四类是独立削减上下限实际上并入第二类约束里。如果有需要还可以加入用户休息间隔约束比如用户连续参与两个时段后必须强制退出至少一个时段这类约束在机组组合里很常见逻辑类似。2.3 求解方法的选型思考这个模型是一个混合整数线性规划问题MILP因为有二进制变量 s_i,t 的存在。Matlab中对应的求解器是 intlinprog集成在Optimization Toolbox里。变量总数为 N×T×2对于小规模算例完全没问题比如20个用户、24个时段变量总数不到1000个intlinprog秒解。但如果你想做大规模仿真比如用户数量上千、时段跨度一个月MILP的求解速度可能会拖后腿。这时有两个方向一是把二进制变量松弛成连续变量变成线性规划问题用 linprog 求解速度非常快缺点是解可能不满足“要么全响应要么不响应”的整数条件二是改用启发式算法比如遗传算法或粒子群算法Matlab的全局优化工具箱也支持但精度和收敛稳定性不如整数规划可靠。我的建议是第一期就用 intlinprog先跑通再优化。代码逻辑清晰检查模型约束特别方便出了问题一眼就能看到是哪个约束出了错。3. Matlab代码实现从数据准备到结果出图的完整流程3.1 数据准备与场景生成我习惯把数据生成单独封装成一个函数这样换场景就像换参数一样简单。下面这段代码生成了24小时内的基线负荷曲线和用户参数表。% generate_ibdr_data.m function [load_base, user_info, D_target] generate_ibdr_data() % 时间设置24小时分辨率为1小时 T 24; t (1:T); % 生成一条典型的夏季负荷曲线晚高峰出现在19-21点 load_base 5000 800 * sin(2*pi*(t-7)/24) ... 1200 * exp(-((t-20).^2)/8) ... 600 * exp(-((t-12).^2)/12); % 用户数量 N 30; % 用户参数削减容量范围[50, 300] kW激励单价范围[0.3, 0.9] 元/kWh rng(2025); % 固定随机种子保证可复现 user_info.id (1:N); user_info.Pmax 50 250 * rand(N, 1); % 最大可削减功率 user_info.Pmin 5 10 * rand(N, 1); % 最小启动削减功率 user_info.price 0.3 0.6 * rand(N, 1); % 申报激励单价 user_info.Kmax randi([4, 8], N, 1); % 最大参与时段数 % 削峰目标高峰时段期望削减量 D_target zeros(T, 1); % 18-21点设为目标削减时段削减量为峰值负荷的8%~12% for k 18:21 D_target(k) load_base(k) * (0.08 0.01 * rand()); end end这里有个细节值得注意基线负荷曲线我用了正弦分量叠加高斯脉冲的方式合成模拟的是一条“基础负荷工业冲击负荷傍晚生活负荷”的合成曲线。如果你手上有真实负荷数据直接替换 load_base 即可后续所有逻辑都不用改。随机种子固定为2025这样每次跑出来的用户参数一致论文里写“实验可复现”就有底气了。3.2 核心求解函数模型落地核心求解函数接收上面生成的数据构造优化模型并调用 intlinprog 求解。这里我用的是Matlab自带的优化模型框架可读性更好。function [Pcut, s, total_cost, exitflag] solve_ibdr_model(load_base, user_info, D_target) T length(load_base); N length(user_info.id); % 决策变量排列顺序先所有连续变量Pcut再所有二进制变量s % 变量总数为 N*T N*T nvar 2 * N * T; % 目标函数系数向量 f zeros(nvar, 1); % Pcut部分系数prices部分系数0s只起逻辑约束作用 for i 1:N idx_p (i-1)*T (1:T); f(idx_p) user_info.price(i) * ones(T, 1); end % 整数变量索引后半部分全部是二进制 intcon (N*T1):nvar; % 边界连续变量下限0上限Pmax二进制变量0-1 lb zeros(nvar, 1); ub inf(nvar, 1); for i 1:N idx_p (i-1)*T (1:T); ub(idx_p) user_info.Pmax(i); end ub((N*T1):end) 1; % 二进制上限1 % 约束矩阵构造稀疏矩阵更高效 % 约束1每时段削减量之和 D_target Aeq zeros(T, nvar); for k 1:T for i 1:N idx_p (i-1)*T k; Aeq(k, idx_p) 1; end end beq D_target; % 约束2Pcut_i_t Pmin_i * s_i_t Pcut - Pmin*s 0 % 约束3Pcut_i_t Pmax_i * s_i_t Pcut - Pmax*s 0 % 每个约束按变量展开 A zeros(2*N*T, nvar); b zeros(2*N*T, 1); rowIdx 0; for i 1:N for k 1:T idx_p (i-1)*T k; idx_s N*T (i-1)*T k; % 约束 A1: Pcut - Pmax*s 0 rowIdx rowIdx 1; A(rowIdx, idx_p) 1; A(rowIdx, idx_s) -user_info.Pmax(i); b(rowIdx) 0; % 约束 A2: -Pcut Pmin*s 0 Pcut - Pmin*s 0 取反 rowIdx rowIdx 1; A(rowIdx, idx_p) -1; A(rowIdx, idx_s) user_info.Pmin(i); b(rowIdx) 0; end end % 约束4每个用户参与总时段数 Kmax for i 1:N rowIdx rowIdx 1; idx_s N*T (i-1)*T (1:T); A(rowIdx, idx_s) 1; b(rowIdx) user_info.Kmax(i); end % 调用求解器 options optimoptions(intlinprog, Display, iter, MaxTime, 120); [x_opt, fval, exitflag] intlinprog(f, intcon, A, b, Aeq, beq, lb, ub, options); if exitflag 0 Pcut reshape(x_opt(1:N*T), T, N); % 每行一个用户每列一个时段 s reshape(x_opt(N*T1:end), T, N); total_cost sum(f .* x_opt); else Pcut zeros(N, T); s zeros(N, T); total_cost inf; error(求解失败请检查约束条件或输入数据); end end代码里的约束矩阵我故意逐行循环构造自然语言思维读起来很直观。但要注意 if 用户规模变大建议改成向量化或稀疏矩阵写法否则矩阵拼接会成为性能瓶颈。还有一个小细节二进制变量的索引范围 intcon 是后半部分连续变量没有整数要求所以 intlinprog 只约束这些变量必须是整数且边界在0-1之间。3.3 参数选择与场景调度策略求解之前还有几个关键参数需要想清楚这些参数直接影响结果的可用性激励单价价格从0.3元/kWh到0.9元/kWh跨度比较大是为了模拟不同用户的不同心理价位。实际项目中这些价格应该是用户真实申报数据或者参考当地需求响应补贴标准。注意总费用是削减电量乘单价如果用户削减了100kW但只持续了半小时那实际电量是50kWh费用要按电量来算。我这里把时段定成1小时所以削减功率乘1小时就是电量目标函数直接累计即可。削峰目标 D_target 设置成峰值的8%~12%是参考了国内部分省份需求响应试点的实际削峰比例一般不超过15%因为削多了用户满意度急剧下降补偿成本也指数上升。市场初期的响应指标往往定在5%左右更稳妥模型里是可配置的我建议先设8%跑通后再往上提。最大参与时段数 Kmax 设成4~8小时来源于协议中约定的“每月最多响应次数折算到单次调度窗口的时长”。如果Kmax设太大模型会偏爱同一个用户反复削减虽然单价低但会过度调用现实中用户会反弹如果Kmax设太小削峰目标可能根本无法达成求解器直接报无解。3.4 结果可视化让削减效果一眼看懂有了决策变量的结果接下来就是画图环节。我做了三张图基线负荷与响应后负荷曲线对比、每个小时的削减功率堆叠图、各时段参与用户的热力图。% visualize_ibdr_results.m function visualize_ibdr_results(load_base, Pcut, s, user_info, total_cost) T length(load_base); N size(Pcut, 1); t (1:T); % 计算响应后的负荷曲线 load_after load_base - sum(Pcut, 1); figure(Position, [100, 100, 1200, 800]); % 子图1负荷曲线对比 subplot(2, 2, 1); plot(t, load_base, k-o, LineWidth, 1.5, DisplayName, 基线负荷); hold on; plot(t, load_after, r-s, LineWidth, 1.5, DisplayName, 响应后负荷); grid on; xlabel(时段/h); ylabel(负荷/kW); title(削峰效果对比); legend(Location, best); % 子图2各时段削减功率堆叠图 subplot(2, 2, 2); x t; y_stack Pcut; % 每列一个用户 area(x, y_stack, LineStyle, -); grid on; xlabel(时段/h); ylabel(削减功率/kW); title(各时段削减功率堆叠); % 子图3用户参与状态热力图 subplot(2, 2, 3); imagesc(s); colormap(parula); colorbar; xlabel(时段/h); ylabel(用户编号); title(用户参与状态热力图); % 子图4各用户激励费用柱状图 subplot(2, 2, 4); user_cost sum(Pcut .* user_info.price, 2); % 各用户总费用 bar(user_info.id, user_cost); grid on; xlabel(用户编号); ylabel(激励费用/元); title([总激励费用: , num2str(total_cost, %.2f), 元]); end堆叠图用area画特别方便一眼就能看出哪些时段削减最多。热力图则能快速发现哪些用户被频繁调用如果某个用户在热力图上横向一整行都是亮的说明这个用户被过度使用了Kmax约束可能需要收紧。4. 优化效果分析从曲线到指标判断模型靠不靠谱4.1 响应前后的负荷曲线对比运行模型后我拿一组典型数据做分析。基线负荷峰值出现在20点约6428kW。模型选择的削减方案将18-21点的负荷压到了5900kW上下削峰幅度约为500kW峰荷削减比例接近8%。这个结果符合设定目标而且高峰时段的削减量精确等于D_target值说明等式约束被严格满足。观察堆叠图会发现18点和19点这两个时段参与削减的用户数量比20点还多原因是这些时段的边际削减成本较低系统优先用便宜资源到了20点便宜资源差不多用完了就必须启动单价更高的用户。这说明模型在“用最小代价完成削峰”的调度逻辑上是正确的并没有出现“高峰期才硬拉高价用户”的情况。4.2 用户参与均衡性评价我常用的一个内部指标是“用户参与均衡度”就是统计每个用户被调用的时段数如果个别用户被调用次数接近Kmax上限而其他用户还是0说明调度存在不均衡。模型加了这个约束后大部分用户的调用次数分布在2~6次之间没有用户顶到上限。这说明约束在设计阶段就避免了“逮着一只羊薅毛”的问题这一点在实际需求响应场景里比单纯降低总成本更重要。4.3 灵敏度实验Kmax变化对结果的影响我尝试把Kmax从4~8改成统一的10重新求解后发现总激励成本确实下降了因为系统可以反复使用低价用户但被调用次数达到8次以上的用户占比超过40%并且这些用户是价格最低的那几个。这在实际中可能引发用户疲劳甚至违约风险。把Kmax压到3以下呢模型直接无解削峰目标完成不了。这个灵敏度实验验证了一个工程经验参与时长约束不是越多越好也不是越少越好而是应该根据用户实际可接受频次来定这比单纯调价格有意义得多。5. 踩坑实录Matlab实现过程中的典型问题与排查思路5.1 intlinprog报告“无解”该怎么办这是我最常遇到的问题几乎每次改约束都会碰到一次。无解的时候intlinprog会返回exitflag -2很多人看到这直接慌了。我总结了一套排查顺序第一步检查等式约束是否自相矛盾。最常见的问题是把D_target设得超过了所有用户削减容量的总和。比如所有用户的Pmax求和只有5000kW但你要求某个时段削减6000kW那必然无解。这个只需要把模型里的等式约束临时改成 (\ge)看求解出的最大削减量是多少立刻就知道是不是容量不足。第二步检查二进制变量的边界条件。如果某个用户Pmin大于Pmax约束 (-PcutPmins0) 和 (Pcut-Pmaxs0) 放在一起当s1时要求Pmin Pmax才会一致否则会有矛盾。这种低级错误在数据清洗阶段就应该拦住。第三步调低MaxTime或者增加容忍度看看求解器是否有进展。intlinprog在长时间找不到可行解时不要把时间耗在那里先用一个简单场景验证模型逻辑没问题再上完整算例。5.2 二进制变量规模大求解速度慢怎么办当用户数量到500、时段到168也就是一周176400个变量时intlinprog会明显吃力。我的优化经验有三个一是把非高峰时段比如凌晨0-6点从求解窗口里剔除约束条件里把D_target设为0的时段全部不参与调度二是把Pcut变量预处理如果某个用户在该时段基线负荷小于Pmin直接把这个变量固定为0三是优先采用“分时段滚动”而不是一次性求解全时段。滚动优化的做法是每4个小时求解一次前一次的边界值作为后一次的初值这样变量规模可以压缩80%以上。5.3 目标函数量纲不统一有一次我粗心地把price_i设置成元/kW而削减量单位是kW时间长度是小时导致目标函数算出来是“元/小时”而不是“元”。这个错误非常隐蔽因为它不会导致求解报错只会让总费用数值看着特别离谱。排查方法很简单把结果里某个用户削减的功率乘以对应单价再乘以时间手算一遍对比程序输出。量纲问题在写论文时尤其容易闹笑话千万别只靠程序跑通就完事。5.4 Matlab老版本没有optimproblem怎么办有些学校的Matlab版本比较旧没有optimproblem这个面向对象的建模接口。这种情况下要么升级到R2017b以上要么退回用intlinprog的低级接口。我上面的代码用的就是低级接口没有依赖新特性只要装了Optimization Toolbox的R2016a以上的版本都能跑。还有一个兼容性细节intlinprog在旧版本里没有MaxTime这个参数的话可以用Maxtime拼写或者直接忽略不影响求解正确性。6. 从静态模型到动态决策几条可行的扩展路径6.1 引入光伏与储能的不确定性当前模型假设负荷预测和削峰目标都是确定值。更真实的场景中光伏出力的波动导致净负荷预测会不断修正。扩展思路是把模型改成分时步滚动优化每个时段开始时用最新预测更新D_target然后重新求解。这种“模型预测控制”思路在Matlab里实现难度不大核心就是加一个循环每次循环调一次优化函数。6.2 多目标优化同时考虑成本和用户舒适度很多课题会要求“最低成本”和“最高用户满意度”两个目标同时优化。可以用带权重的加权法把多目标转成单目标也可以用fgoalattain或gamultiobj绘制帕累托前沿。我建议先做加权法缺点是要手动调权重帕累托前沿的好处是能看到完整解集给决策者更多选择空间但计算量成倍增加。实际上把用户舒适度转成约束条件比如每个用户削减量不超过基线负荷20%比做成目标函数更符合工程习惯模型也稳定得多。6.3 数据驱动基线负荷生成基线负荷的准确性直接影响削减量的核算。现在很多研究使用类似“近期相似日平均负荷”的方法或者用机器学习回归预测基线。Matlab里可以接入回归模型、时间序列模型甚至深度学习工具箱的LSTM网络来预测基线负荷。不过要提醒一点不管预测模型有多先进需求响应结算通常有标准化的基线认定流程模型精度太高反而不一定被认可校内仿真随便用实际项目要跟结算规则对齐。7. 对Matlab编程素养的几点真心建议跑完这个项目我最大的体会是Matlab不是简单地把公式敲进去就能出结果它要求你有清晰的变量视角和矩阵思维。变量全放在workspace里随时whos查一下维度模型的每个矩阵都要能画出来、能算出来、能一眼看懂。我用了一个星期才把“决策变量到底怎么排列”这件事想明白一旦想透了后面所有约束条件都手到擒来。另一个心得是代码注释不要省。这个模型里每个矩阵的每一行什么意思、每个变量索引怎么对应回用户和时段如果当时不写清楚隔两个星期自己都看不懂。我现在的习惯是每个函数头部写清楚输入输出每个约束块的上方写注释公式和发表论文里的公式编号一一对应。最后说一个小技巧优化工具箱的intlinprog返回的x_opt是一大串向量直接reshape之前一定要先打印几个关键位置的值检查一下别等画完图才发现数据排列错了。我吃过这个亏20个用户的数据reshape错了一列画出来的堆叠图组合在一起看好像没问题逐用户检查才发现南辕北辙。这个模型后续我还打算接入真实负荷数据把节点电价引入日前市场和日内市场再叠加一个储能调度模块到时候再开一篇新文章分享。眼下这份代码和思路足够把激励型需求响应的大部分场景复现出来了。如果你也在这个方向摸索希望这篇记录能帮你少走几步弯路。