最近在整理一个电力系统优化调度的仿真项目,核心是“日前-日内两阶段优化调度”,并且针对电价、光伏出力、风电出力、负荷这四个参数分别做了独立的敏感性分析,整套代码用Matlab实现。这个项目搭起来以后,既能用来做新能源并网调度的基础研究,也能给课题组新同学快速讲清楚两阶段调度的原理和实现路径。今天不绕弯子,把模型设计、敏感性分析思路、Matlab代码结构和调试经验完整梳理一遍。
为什么我要特别强调“独立”的敏感性分析?因为实际做仿真时,如果每次把几个参数一起调,结果出来以后很难判断到底是哪个因素在起主导作用。分开扰动之后,每个参数的影响边界一目了然,也方便后面写论文或者做工程汇报时直接引用。
1. 项目整体思路与两阶段调度模型设计
1.1 为什么非得是“日前-日内”两阶段
先说实话,很多刚接触调度优化的同学会问:直接用实时调度不行吗?或者只用日前调度,把预测做得准一点,是不是就够了?
问题在于预测不可能完全准。光伏出力受云层影响,风电出力波动更大,负荷本身也有很强的随机性。如果只看日前预测,当天实际的新能源出力可能跟预测差到百分之二三十,这时日前给出的机组组合可能不够用,或者多余机组白白启动,导致运行成本飙升。反过来,如果只依靠实时调度,机组启停、最小运行时间这类决策又没办法临时拍板,因为火电机组启动需要时间,储能也需要提前预留空间。
所以两阶段调度的本质是“计划+修正”:日前阶段先把机组启停、备用安排、储能基准计划定下来,日内阶段根据最新的预测数据,在不动机组启停的前提下,快速调整出力、充电放电和购电功率。这样做既保留了日前优化的全局经济性,又兼顾了日内应对不确定性的灵活性。
类比一下:日前调度像周末策划一周的菜谱,日内调度像当天打开冰箱看还剩什么,临时调整菜量和菜品。两者配合,才能既不太浪费,又不会到饭点没菜吃。
1.2 模型目标函数和约束条件
这个项目搭建了一个简化但完整的电力系统模型:包含3台常规火电机组、一组储能、光伏电站、风电场,以及一个外部电网购电接口。日前调度以1小时为步长,优化未来24小时;日内调度以15分钟为步长,滚动优化未来4小时。为了降低求解难度,日内调度固定了日前确定的机组启停状态,只优化连续变量。
目标函数可以写成混合整数线性规划(MILP)形式:
min ∑ₜ [ ∑ᵢ (aᵢ·Pᵢ(t) + bᵢ·uᵢ(t)) + ∑ᵢ SUCᵢ·vᵢ(t) + price_buy(t)·P_buy(t) ]
其中第一项是机组燃料成本和固定运行成本,第二项是机组启动成本,第三项是向外部电网购电的成本。如果允许弃风弃光,还需要加上弃电惩罚项,用于避免系统为了省成本而故意削减新能源出力。
约束条件主要包括:
- 功率平衡约束:所有机组出力、光伏、风电、购电、储能放电之和,等于负荷加上储能充电功率和弃电功率;
- 机组出力上下限约束:P_min·u ≤ P ≤ P_max·u;
- 机组爬坡约束:相邻时段出力变化不能超过爬坡速率;
- 最小启停时间约束:机组一旦启动或停机,需要保持一定时间;
- 储能SOC递推约束:SOC(t+1) = SOC(t) + η_ch·P_ch(t) - P_dis(t)/η_dis,同时SOC和充放电功率都有上下限;
- 购电功率上限约束。
这些约束在Matlab中用YALMIP写起来非常直观,后面在代码部分会展开。
1.3 数据假设与基准场景设置
仿真数据要有一个固定的“基准场景”,这是做敏感性分析的前提。我采用的是一组典型日曲线:负荷呈现早高晚高的双峰形状,光伏出力中午单峰,风电出力由随机过程生成但波动幅度较大,电价采用分时电价,高峰电价约为低谷的2.5倍。
系统基准容量取100MW,所有数据都换算成标幺值,这样求解器处理起来数值稳定性更好。机组参数方面,3台机组的容量分别取50MW、80MW、100MW,煤耗线性系数取0.035、0.028、0.025($/MWh),启动成本取300、500、700($)。储能容量30MWh,最大充放电功率10MW,效率0.95。这些参数不需要苛求精确,但需要保证基准场景可行,否则后续参数扰动很容易出现无解。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 四参数独立敏感性分析的设计思路
2.1 为什么单独做“独立”敏感性分析
在电力系统优化问题里,电价、光伏、风电、负荷四个参数几乎决定了调度结果的基本走向。但如果一次性把四个参数都改了,比如电价升高10%、光伏降低20%、风电升高15%、负荷升高5%,最后总成本变动了10%,你能说出这是哪个参数的作用吗?很难。
所以要采用控制变量法:每次只改变一个参数,其他三个固定在基准值,分别观察系统响应。这样做出来的曲线干净、可解释性强。比如电价从0.8倍逐步增加到1.2倍,其他三个参数不变,这时候总成本的变化就完全由电价引起,可以直接计算敏感度系数。
2.2 四个参数的扰动方式和边界选取
扰动方式上,我建议对每个参数序列整体乘以一个比例系数,而不是简单加减固定值。原因是四个参数的绝对量级差异很大——电价可能是0.3元/kWh,负荷可能是50MW,如果统一加10,完全没有可比性;按比例缩放后,横轴就变成“相对基准的倍率”,四个参数可以在同一张图里比较。
我设置的扰动范围是0.8到1.2,步长0.05,也就是每个参数取9个点。这个范围基本覆盖了实际可能出现的预测偏差:负荷预测误差一般不会超过±20%,光伏预测误差可能略大,风电误差甚至更大。如果只是想看趋势,这个范围够了。如果想捕捉非线性突变,可以扩大到0.5到1.5,但要注意极端参数下模型可能会无解。
参数缩放对模型的影响各有不同:
- 电价:直接改变购电项在目标函数里的权重。电价整体抬高后,系统会倾向于减少购电,多用本地机组和储能;
- 光伏:改变光伏出力曲线。光伏出力增大后,系统可能面临更多的弃光问题,尤其是在午间负荷低谷时段;
- 风电:改变风电出力曲线。风电波动大,出力增大后对爬坡和备用需求更敏感;
- 负荷:改变负荷曲线。负荷上升会迫使系统启动更多机组,甚至会触发失负荷惩罚。
2.3 结果指标和敏感度量化
敏感性分析不能只看曲线趋势,还需要一个可比较的数字。我用的核心指标是总成本,辅助指标是弃风弃光率、失负荷率和储能日均循环次数。
敏感度系数的定义:S = (Δy/y₀) / (Δx/x₀)。例如电价从基准倍率1.0提升到1.1,总成本上升5%,那么电价对总成本的敏感度系数就是0.5。对四个参数分别计算后,可以做一个柱状图,一眼看出哪个参数对总成本影响最大。在我这个系统设定下,负荷和电价的敏感度往往最高,光伏和风电的敏感度相对低一些,但这不是固定结论,跟系统内可控电源的调节能力、储能配置都有关系。
弃电率这个指标特别值得关注。有时候总成本下降得很漂亮,但弃风弃光率大幅上升,说明系统是靠“扔掉新能源”来降低成本,这是不健康的调度结果。所以在敏感性分析里,我一般会把成本和弃电率放在一起看,避免被单一指标误导。
2.4 结果可视化:曲线、柱状图和单位敏感度
Matlab的可视化工具足够用了。我的做法是:
- 画四张折线图,横轴都是参数倍率,纵轴分别是总成本和弃电率,用subplot排列;
- 再画一张汇总柱状图,横轴是四个参数,纵轴是敏感度系数;
- 如果需要投稿,可以把总成本和弃电率做成双轴图,用yyaxis实现。
这批图生成后,整个项目的结论就非常清晰了。比如某个参数在0.8-1.2范围内,总成本几乎不变,说明系统对该参数不敏感,后续做不确定性分析时可以不优先考虑它;反之,如果某个参数稍微变化,成本和弃电率就剧烈波动,那它一定是系统规划和运行中最需要关注的风险源。
3. Matlab实现要点与关键代码片段
3.1 程序目录结构和求解器选型
这个项目我分成四个脚本:
- data_generate.m:生成基准数据,包括负荷、光伏、风电、电价;
- run_two_stage.m:两阶段调度主函数,输入四个参数序列,输出总成本和弃电率;
- sensitivity_analysis.m:循环调用run_two_stage,完成四参数敏感性分析;
- plot_results.m:把结果画成图表。
求解器方面,如果你的Matlab版本自带Optimization Toolbox,可以直接用intlinprog求解MILP。如果安装了YALMIP和Gurobi,建模会方便很多,因为目标函数和约束可以用符号表达式直接写,不容易错。我自己习惯用YALMIP + Gurobi,Gurobi求解MILP的速度比内置求解器快不少,尤其在做敏感性分析这种批量求解任务时,速度优势非常明显。
3.2 数据生成与参数封装的技巧
把基准数据存成结构体,后续传参和修改会非常方便:
matlab复制base = struct();
base.T = 24;
base.n_units = 3;
base.load = [5.6 5.4 5.2 5.0 5.1 5.8 6.4 7.0 7.4 7.2 6.8 6.6 6.9 7.1 7.5 7.6 7.8 8.0 7.9 7.5 7.0 6.5 6.2 5.8];
base.pv = [0 0 0 0 0.1 0.9 2.4 4.2 5.8 6.5 6.2 5.5 4.8 3.6 2.1 0.8 0.1 0 0 0 0 0 0 0];
base.wind = [2.1 2.3 2.5 2.4 2.2 2.0 1.8 1.6 1.7 1.9 2.2 2.5 2.8 3.0 3.1 2.9 2.7 2.4 2.2 2.0 1.9 1.8 1.9 2.0];
base.price = [0.35 0.35 0.35 0.35 0.35 0.4 0.55 0.7 0.75 0.7 0.6 0.5 0.5 0.55 0.6 0.65 0.7 0.8 0.85 0.75 0.6 0.45 0.4 0.35];
base.Pmin = [15 20 30];
base.Pmax = [50 80 100];
base.Ramp = [20 25 30];
base.StartCost = [300 500 700];
base.SOC_init = 0.5;
base.SOC_min = 0.2;
base.SOC_max = 0.9;
base.Eta_ch = 0.95;
base.Eta_dis = 0.95;
注意,数据长度是24,对应日前调度的24小时。如果要做日内调度,我会单独生成一个15分钟粒度的数据结构,然后把日前启停状态传进去。这里有个容易踩的坑:从结构体复制基准数据时,不要直接写new_base = base,因为Matlab在修改一个字段时不会影响原结构体,但如果字段里存了矩阵,直接用base.load = ...修改可能造成引用混淆。更稳妥的方式是写一个函数,输入scale参数,返回修改后的结构体。
3.3 两阶段求解的主流程代码
用一个函数封装两阶段调度的完整流程:
matlab复制function [result] = run_two_stage(base, scale)
% scale: 结构体,包含 price_scale, pv_scale, wind_scale, load_scale
s = base;
s.load = base.load * scale.load_scale;
s.pv = base.pv * scale.pv_scale;
s.wind = base.wind * scale.wind_scale;
s.price = base.price * scale.price_scale;
% 阶段1:日前调度
[da_sol, da_cost] = solve_day_ahead(s);
% 阶段2:日内调度,固定日前机组启停
[id_sol, id_cost] = solve_intraday(s, da_sol.u);
result.total_cost = da_cost + id_cost;
result.curtailment = da_sol.curtail + id_sol.curtail;
result.unserved_load = id_sol.unserved_load;
end
这种封装方式的好处是敏感性分析主循环里只需要修改scale结构体的某个字段,其他都交给函数处理,代码整洁,也方便扩展。
3.4 日前优化建模的YALMIP代码示例
下面给出一个精简但完整可跑的日前调度YALMIP建模片段:
matlab复制function [sol, cost] = solve_day_ahead(s)
T = s.T;
n = s.n_units;
% 变量定义
P = sdpvar(n, T, 'full'); % 机组出力
u = binvar(n, T, 'full'); % 启停状态
v = binvar(n, T, 'full'); % 启动动作
P_buy = sdpvar(1, T, 'full'); % 购电功率
P_ch = sdpvar(1, T, 'full'); % 储能充电
P_dis = sdpvar(1, T, 'full'); % 储能放电
SOC = sdpvar(1, T+1, 'full'); % 储能SOC
Constraints = [];
% 启动动作约束:v(t)=1 当且仅当 u从0变1
Constraints = [Constraints, v(:,1) >= u(:,1)];
for t = 2:T
Constraints = [Constraints, v(:,t) >= u(:,t) - u(:,t-1)];
end
Constraints = [Constraints, v >= 0, v <= 1];
% 功率平衡
Constraints = [Constraints, sum(P,1) + s.pv' + s.wind' + P_buy + P_dis == s.load' + P_ch];
% 机组出力上下限
Constraints = [Constraints, repmat(s.Pmin',1,T).*u <= P <= repmat(s.Pmax',1,T).*u];
% 爬坡约束
Constraints = [Constraints, P(:,2:end) - P(:,1:end-1) <= repmat(s.Ramp',1,T-1)];
Constraints = [Constraints, P(:,1:end-1) - P(:,2:end) <= repmat(s.Ramp',1,T-1)];
% 储能约束
Constraints = [Constraints, SOC(2:end) == SOC(1:end-1) + s.Eta_ch*P_ch - P_dis/s.Eta_dis];
Constraints = [Constraints, SOC >= s.SOC_min, SOC <= s.SOC_max];
Constraints = [Constraints, P_ch >= 0, P_ch <= 10, P_dis >= 0, P_dis <= 10];
% 目标函数
fuel_cost = sum(sum(repmat([0.035 0.028 0.025]',1,T) .* P));
start_cost = sum(sum(repmat(s.StartCost',1,T) .* v));
buy_cost = sum(s.price .* P_buy);
curtail_penalty = 1000 * sum( s.pv' + s.wind' + P_dis - P_buy ); % 惩罚项示例,实际需要额外变量
Objective = fuel_cost + start_cost + buy_cost;
ops = sdpsettings('solver','gurobi','verbose',0,'gurobi.MIPGap',0.01);
optimize(Constraints, Objective, ops);
sol.P = value(P);
sol.u = value(u);
sol.v = value(v);
sol.P_buy = value(P_buy);
sol.SOC = value(SOC);
sol.P_ch = value(P_ch);
sol.P_dis = value(P_dis);
cost = value(Objective);
end
这里需要说明一点:上面代码里的curtail_penalty只是示意,实际上如果想正确计算弃风弃光量,需要额外引入弃电变量,比如P_curtail = s.pv + s.wind - (s.load - sum(P) - P_buy),然后在目标函数里加惩罚。我建议在建模时直接定义弃电变量,这样功率平衡约束变成:
matlab复制sum(P,1) + s.pv' + s.wind' + P_buy + P_dis == s.load' + P_ch + P_curtail;
其中P_curtail就是弃电功率,目标函数加入penalty * sum(P_curtail)。这样既能保证模型有解,也能统计弃电率。
3.5 日内滚动优化的实现
日内调度以15分钟为步长,滚动周期为4小时。完整滚动优化的代码量会比较大,需要维护一个预测更新循环。如果只是做敏感性分析,可以采用简化方式:日内阶段以15分钟为步长求解一次,时间范围是未来4小时,但忽略滚动更新,只作为“日内修正”。
要注意的是,日前调度的决策变量是小时级,日内调度是15分钟级。日内阶段需要用线性插值把日前计划转换成15分钟参考轨迹,或者直接把日前机组启停状态重复扩展到每个15分钟时段。我在代码里采用了后者:u_id = kron(da_sol.u, ones(1,4)),这样最简单,也不容易出错。
日内优化问题的目标函数可以增加一个“调整惩罚”,避免日内出力跟日前计划偏差过大:
matlab复制penalty_adjust = 0.2 * sum(sum(abs(P_id - P_da_ref)));
这里P_da_ref是日前计划的小时出力插值到15分钟后的参考值。也可以用二次惩罚,但如果用线性规划就保持线性惩罚项。加入这个惩罚后,模型不会因为日内预测变化而大幅度偏离日前计划,更符合实际调度偏好。
3.6 敏感性分析主循环与数据记录
主循环的代码非常简洁:
matlab复制scales = 0.8:0.05:1.2;
params = {'price_scale','pv_scale','wind_scale','load_scale'};
param_names = {'电价','光伏','风电','负荷'};
n_scale = length(scales);
n_param = length(params);
cost_matrix = zeros(n_param, n_scale);
curtail_matrix = zeros(n_param, n_scale);
for p = 1:n_param
for k = 1:n_scale
scale = struct('price_scale',1,'pv_scale',1,'wind_scale',1,'load_scale',1);
scale.(params{p}) = scales(k);
res = run_two_stage(base, scale);
cost_matrix(p,k) = res.total_cost;
curtail_matrix(p,k) = res.curtailment;
fprintf('%s x%.2f, cost=%.2f, curtail=%.2f\n', params{p}, scales(k), res.total_cost, res.curtailment);
end
end
这段代码直接把结果存进矩阵,后面画图就很方便。特别提醒:在循环里每次都要新建scale结构体,否则上一次循环修改的字段会残留,导致多个参数同时被扰动。
3.7 求解性能优化
如果直接把日前T=24、日内T=96点全部做成MILP,再循环25次,速度会很感人。我在实际运行中用了几个优化手段:
- 日前T=24,日内T=16(4小时×15分钟),这样MILP规模大幅下降;
- 日内阶段没有整数变量,是一个纯LP,速度非常快;
- 给Gurobi设置
MIPGap=0.01,也就是允许1%的次优解,换来几十倍的速度提升; - 使用热启动:在敏感性分析循环中,把上一轮求解的机组启停状态作为下一轮初始解,可以进一步提速。但这个实现起来稍微复杂,我在工程版里用了,这里不再展开。
实测下来,3台机组、24小时日前+4小时日内,一组参数大概0.5秒,整个四参数敏感性分析25个点不到30秒,完全可接受。
4. 常见问题与排查技巧实录
4.1 求解报错“Infeasible problem”
这是最常见的问题,几乎每次调新参数都会碰到。原因一般是约束冲突,比如负荷太高但机组最大出力不够,或者储能SOC初始值不在上下限范围内,或者机组最小启停时间约束太强。
解决办法有几个:首先是检查数据,把负荷、光伏、风电曲线画出来,看看有没有负值或者特别离谱的尖峰。其次是检查初始SOC,如果SOC下限是0.2,而SOC_init设成了0.1,模型必然无解。第三是把功率平衡硬约束改成带惩罚的软约束,比如允许失负荷,但失负荷量乘以一个很大的惩罚系数,这样即便数据极端,求解器也能找到一个次优解,整个敏感性分析循环不至于中途崩溃。
4.2 两阶段结果对不上
日内调度应该在日前结果基础上运行,但很多初学者直接把两个问题独立求解,导致日内优化给出的机组出力在日前停机时段仍然有数值。调试方法很简单:在日内求解完成后,检查P_id <= Pmax * u_da是否成立,如果不成立,说明你忘了把日前启停变量传到日内约束里。
另一个容易错的地方是时间粒度对齐。日前是24点,日内是96点,如果你直接拿日前曲线跟日内曲线逐点比较,会错位。正确的做法是把日前状态扩展到15分钟,可以用kron(u_da, ones(1,4)),或者用interp1做线性插值。
4.3 敏感性曲线有跳变或不符合直觉
如果成本随光伏增大反而增大,第一反应不应该是怀疑物理规律,而是检查模型里有没有把弃电惩罚加进去。光伏出力增大后,如果系统消纳不了,目标函数又允许无成本弃光,那么成本确实可能不变甚至下降;但如果加了惩罚,成本可能会因弃光量增大而上升。另外,如果随机的风速序列没有固定种子,每次循环都会产生一条新的风速曲线,会导致结果抖动得没法看。做敏感性分析前,一定用rng(2025)固定随机种子。
4.4 求解速度太慢
之前遇到过T=96点+MILP+暴力循环,跑了40分钟都没出结果。后来做了三件事:日前分辨率设为1小时,日内只做4小时滚动;把机组数量从5台精简到3台;给Gurobi设置MIPGap=0.01。结果一组完整循环不到1分钟。如果只是做趋势分析,不需要追求极高精度,模型简单一点反而更能说明问题。
4.5 常见问题速查表
| 现象 | 可能原因 | 排查/解决 |
|---|---|---|
| 提示求解器未找到 | YALMIP没装对应求解器 | 运行yalmiptest确认Gurobi/CPLEX已配置 |
| 结果全部为零 | 目标函数写错或变量未连接 | 打印约束长度,检查目标函数里的系数 |
| 储能一直不充放电 | 电价高峰/低谷差太小,或者SOC约束过紧 | 拉大分时电价差,检查SOC上下限 |
| 两阶段总成本等于单阶段 | 日内没使用日前启停变量 | 检查日内约束是否包含u_da |
| 敏感性曲线毛刺多 | 随机数未固定,预测曲线每次不同 | 增加rng(固定值),保证重复性 |
这些坑我基本都踩过,尤其“两阶段总成本等于单阶段”这个问题,表面看起来结果没变,其实是因为日内阶段完全没约束,直接把日前结果重复了一遍。调试时一定要打印出日前和日内各自的成本,分开看才能发现异常。
4.6 最后的实操心得
做这个项目最花时间的不是两阶段模型本身,而是把敏感性分析做成可复现的批量流程。每个参数一改,整个优化都要重跑,如果模型不够稳,某个点失败整个循环就崩了。我的建议是在循环里加try-catch,即使某个参数点失败也能继续,并记录错误信息。另外,画图时横轴用倍率而非绝对数值,纵轴用“总成本相对基准的百分比变化”,这样四个参数可以在同一张图里比较,干扰也小。
最后再分享一个小技巧:在完成四参数敏感性分析后,可以顺手把结果导出成Excel或者CSV。后面写论文或者调整模型参数时,旧结果就不用重新再跑一遍了。我通常会把敏感度系数、各参数下的成本、弃电率都存到一个表格里,方便随时回看和对比。
