简介:一份面向电气工程、电力系统方向毕业设计及研究者使用的虚拟电厂/微网日前随机优化调度源程序,重点解决光伏出力与负荷功率双重不确定条件下的调度决策问题。模型基于随机规划法构建,结合蒙特卡洛算法生成预测场景,并通过快速概率距离消除法将场景削减至5个典型场景,再对不同场景下的调度策略进行优化,程序整体可运行且结果清晰,适合用于复现论文、拓展模型或作为毕业论文的程序支撑。包体方面,资源以rar压缩包形式提供,大小约3.66MB,便于直接下载解压使用;具体文件清单因上游未提供暂无法列示,主体为源程序文件。目前已有87人学习浏览,内容与知网论文《计及源荷双重不确定性的虚拟电厂微网日前随机优化调度》相对应,可配合论文对照理解模型构建、场景削减与优化调度全过程,并为交直流混合配电网等多阶段随机调度研究提供参考。
1. 虚拟电厂微网日前随机优化调度:把「源荷不确定性」显式写进调度计划
如果只按负荷预测平均值排一遍微型燃气轮机和储能,白天光伏一被云遮、傍晚负荷突然抬高,功率平衡就会瞬间失守。这就是微网日前调度最常见的翻车现场:计划做得漂亮,实际跑起来却要靠储能和主网交互兜底。标题里这套 141 号源程序资源,配合知网可下载的同名论文,解决的就是「计及源荷双重不确定性的虚拟电厂微网日前随机优化调度」——把风电、光伏和负荷的随机波动用多个场景显式纳入优化模型,让微网在明天可能存在多种运行情况的前提下,提前把机组出力、储能充放电、可调负荷和主网购电计划安排出来。适合正在复现虚拟电厂与微网调度论文、想搞懂随机优化在程序里如何落地,或准备把这类算例用进毕业设计与项目初评的研究生和工程师。
2. 源荷双重不确定性建模:从概率分布到典型场景的完整链路
2.1 电源侧与负荷侧不确定性的数学表达
虚拟电厂微网里,不确定源通常来自三处:风电场出力、光伏阵列出力、负荷预测误差。日前调度的时间颗粒度一般是 1 小时,所以对每个时段 t=1,...,24,风电出力被认为是一个随机变量。最常见的做法是用 Weibull 分布描述风速,再经过风机功率曲线映射成出力;光伏则用 Beta 分布描述辐照度,再乘上光电转换效率。负荷预测误差一般假定服从截断正态分布,标准差取预测值的某个比例,例如 5% 到 10%。
源荷双重不确定性在一起时,不能简单把两边的期望值相加。原因是光伏高峰通常对应白天的负荷高峰,晚间风电大时负荷又低,两边不是独立波动。如果只用期望值,忽略相关性,调度出来的备用容量要么偏高浪费掉,要么偏低导致实际运行风险。所以在程序里常见的是:用历史预测误差样本的经验协方差矩阵,或者用每个时段的独立分布做蒙特卡洛采样,然后再用场景削减把成百上千个原始场景压缩成十来个典型场景,后续求解才能扛得住。
参数上,我一般会让你先确认三张表:风机额定功率与切入、切出风速,光伏峰值功率与 Beta 分布的形状参数,以及负荷预测误差的标准差比例。这三组参数直接在场景生成脚本里改,后面不需要动模型主体。如果你复现的是论文配套程序,这一步最容易对不上:论文给的是最终场景曲线,而中间采样步骤每个人用的随机种子不同,画出来的曲线自然不完全一致,但只要统计特征接近,调度结果就应当可复现。
2.2 场景生成与削减:把随机优化变成可解的确定性等价
严格地说,随机优化是含有随机变量和期望目标的问题。直接用连续概率分布去建模,解起来非常困难。工程上的标准路径是场景法:第一步,用蒙特卡洛或者拉丁超立方采样,对每个时段的每个随机变量生成一批样本。注意不要对每个时段独立采样后简单拼接,而应当用拉丁超立方等方法保证时段间样本的均匀覆盖。第二步,对生成的原始场景集合做削减,用同步回代削减或快速前向选择,把初始场景数压缩为少量典型场景,并且给每个典型场景分配一个概率权重。
这里有两个关键参数直接决定求解质量和速度:原始场景数与削减后场景数。原始场景数太小,极端场景抓不全;太大,削减算法耗时呈平方上升。我在类似算例中常用初始场景数 1000,削减后场景数取 10 或 20。取 10 时求解飞快,但结果会比取 20 略差;取 30 以上时模型规模会明显膨胀,具体取舍放到第 4 章的避坑部分再展开。
削减后每个场景获得概率 p_s,且所有概率之和为 1。于是随机优化问题可以写成期望值形式的目标函数加对每个场景分别成立的约束,这就是所谓确定性等价。程序里的随机模型实际上是在这份典型场景集合上展开的,这也是为什么随机优化调度程序的代码结构里,所有核心约束都会多出一个「场景维度」。
2.3 日前随机优化调度的目标函数与约束拆解
模型的目标函数常见写法是系统日运行期望成本最小,包含四类成本:第一,购电成本与售电收益。微网通过公共连接点与大电网交换功率,不同时段电价不同,购售电状态一般要做互斥约束,否则程序会同时买和卖,赚取无意义的电费差价。第二,微型燃气轮机的燃料成本,可以用二次函数或者线性化分段表示。第三,可调负荷的中断补偿成本,通常按下调电量和单位赔偿单价乘积计算。第四,弃风弃光惩罚成本,有时还会加储能日常运行维护成本。
核心约束包括:每个时段每个场景下的功率平衡约束;微型燃气轮机出力上下限和爬坡约束;储能充放电功率与荷电状态约束,且一天结束时的荷电状态要回到初始值附近;可调负荷的削减量上下限约束;与主网交互功率上限约束。随机优化的独特之处在于,功率平衡、储能、爬坡这类约束要对每个场景分别成立,而日前开停机状态和购售电状态这类「第一阶段决策」必须跨场景保持一致。这个区别决定程序是真正的随机优化,而不是多个确定性问题的简单并列,也是程序调试时最容易踩坑的位置。
3. 用 MATLAB+YALMIP+CPLEX 复现:场景展开、建模与求解的完整代码
3.1 场景集合与输入参数的准备代码
复现这类程序,最常见的技术栈是 MATLAB + YALMIP + CPLEX。YALMIP 负责建模,CPLEX 负责解这个混合整数线性规划。下面这份代码是场景装载和公共参数部分。注意它依赖你已经在前面用场景生成脚本算好的三份矩阵:load_scen、wind_scen、pv_scen,维度均为 S 行 T 列。
%% 场景与公共参数输入(示意框架,按你的数据文件修改变量名即可) S = 20; % 场景削减后的典型场景数 T = 24; % 日前调度时段数,按小时计 Pt_load = load_scen; % S x T:典型负荷场景(kW) Pt_wind = wind_scen; % S x T:典型风电出力场景(kW) Pt_pv = pv_scen; % S x T:典型光伏出力场景(kW) %% 分时电价与成本参数 buy_price = linspace(0.4, 0.9, T); % 示例:1xT 购电价(元/kWh) sell_price = linspace(0.3, 0.7, T); % 示例:1xT 售电价(元/kWh) gas_price = 0.65; % 燃气轮机燃料成本折算(元/kWh) cut_compensation = 1.5; % 可调负荷削减赔偿单价(元/kWh) penalty_curtail = 0.8; % 弃风弃光惩罚单价(元/kWh) Price_buy = buy_price' * ones(1, S); % T x S,与决策变量同维度 Price_sell = sell_price' * ones(1, S); % T x S %% 机组与储能参数 Pmt_max = 300; % MT 最大出力(kW) Pmt_min = 30; % MT 最小技术出力(kW) Ramp_up = 60; % 每小时爬坡上限(kW/h) Eess_max = 600; % 储能容量(kWh) Pch_max = 150; % 最大充电功率(kW) Pdis_max = 150; % 最大放电功率(kW) eta_ch = 0.95; % 充电效率 eta_dis = 0.95; % 放电效率 SOC_init = 0.5; % 初始荷电状态(百分数) SOC_end = 0.5; % 调度周期末荷电状态目标这段代码的意义不是计算,而是把模型与数据解耦。随机优化的程序调试,90% 的精力都花在确认这些矩阵的维度对不对、量纲是不是一致上。写程序时我习惯把所有外接数据统一成 S 行 T 列的矩阵,并且全部转成 kW 和 kWh,避免后面约束里出现 1e3 级别的系数隐藏坑。Pmt_min 是燃气轮机的最小技术出力,很多人初版只写下限 0,这会让燃气轮机在低负荷段落频繁开关机,和目标函数里的启停成本一起产生不真实的调度弧线。
3.2 随机优化模型的 YALMIP 建模代码
下面这段是模型主体的核心。需要提前强调,这个程序里我们把微型燃气轮机出力、储能充放电功率、可调负荷削减量、购售电功率都定义成场景相关变量;而燃气轮机启停状态、储能充放电模式是场景无关的,这是两阶段随机优化的典型结构。
%% 决策变量声明 % 第一阶段:场景无关,日前就要定下来 u_MT = binvar(T, 1); % 燃气轮机启停状态,1表示运行 u_sell = binvar(T, 1); % 售电状态,1表示允许售电,用于购售电互斥 % 第二阶段:随场景调整,应对不同场景的出力/负荷偏差 P_MT = sdpvar(T, S); % MT出力,场景相关 P_buy = sdpvar(T, S); % 向主网购电 P_sell = sdpvar(T, S); % 向主网售电 P_ch = sdpvar(T, S); % 储能充电功率 P_dis = sdpvar(T, S); % 储能放电功率 u_ess_charge = binvar(T, S); % 储能充电状态(场景相关),1充电0放电 SOC = sdpvar(T+1, S); % 荷电状态,多一行存放初值 P_curtail_w = sdpvar(T, S); % 弃风功率 P_curtail_pv = sdpvar(T, S); % 弃光功率 P_load_cut = sdpvar(T, S); % 可调负荷削减量把 MT 出力定义为场景相关,理论含义是:日内调度可以根据实际场景做再调整,但启停状态是日前就要公布的决策,不能等场景出现后再变。这样建模的结果是,MT 出力在一个时段内会在不同场景下取不同值,而 u_MT 保持一致。如果初学者把所有变量都设成与场景无关,那等于退化成确定性调度;如果全部设成场景相关,又可能让程序为每个场景选择不同的启停状态,这在日前市场框架下是不可执行的。理解这一层,是看懂程序架构的前提。
3.3 约束集合的完整建模
接下来写约束。我按模块组织:功率平衡、MT运行、储能、可调负荷、电网交互。这些约束要用矩阵方式写到 YALMIP 的约束集合里,逐约束写比较直观。
F = []; %% 功率平衡:每个场景每个时段都必须成立 F = [F, P_MT + P_dis - P_ch + P_buy - P_sell + Pt_wind + Pt_pv ... == Pt_load - P_load_cut + P_curtail_w + P_curtail_pv]; %% 燃气轮机运行约束 F = [F, P_MT <= Pmt_max * u_MT]; F = [F, P_MT >= Pmt_min * u_MT]; F = [F, P_MT(2:T,:) - P_MT(1:T-1,:) <= Ramp_up]; F = [F, P_MT(1:T-1,:) - P_MT(2:T,:) <= Ramp_up]; %% 储能约束 F = [F, P_ch <= Pch_max * u_ess_charge]; F = [F, P_dis <= Pdis_max * (1 - u_ess_charge)]; F = [F, 0 <= P_ch, 0 <= P_dis]; F = [F, SOC(2:T+1,:) == SOC(1:T,:) ... + P_ch .* eta_ch ./ Eess_max - P_dis ./ eta_dis ./ Eess_max]; F = [F, SOC(1,:) == SOC_init, SOC(T+1,:) == SOC_end]; F = [F, 0 <= SOC <= 1]; %% 电网交互与可调负荷 F = [F, 0 <= P_buy <= 1000, 0 <= P_sell <= 500]; F = [F, P_buy <= 1000 * repmat(1 - u_sell, 1, S)]; % 购售电互斥,避免同时买卖套利 F = [F, P_sell <= 500 * repmat(u_sell, 1, S)]; F = [F, 0 <= P_load_cut <= 0.15 * Pt_load]; % 最多削减15%负荷功率平衡约束里,我把弃风 P_curtail_w 和弃光 P_curtail_pv 放在等式左边,意思是源侧多发出的部分可以作为丢弃项兜底,这样模型不会因为场景里风电瞬间超过负荷而不可行。储能公式里的 SOC 增量必须除以 Eess_max,单位才对齐到百分数荷电状态。注意 SOC 的行数多了一个,第 1 行放初始值,第 T+1 行放终值,这样 SOC(2:T+1,:) 对应的才是时段 1 到 24 结束后的状态。
这里还有一处细节:u_sell 是 T×1 维,和 S 个场景全部共享。它表示某个时段允许售电还是购电,这是由日前决策决定的,不随场景变化。如果省掉这组约束,程序在高价时段可能同时买电和卖电,目标函数出现套利空间,结果虽然数学上最优,物理上却不可执行。
3.4 目标函数与求解设置
目标函数用期望形式求和。为了代码简洁,这里用均匀概率 1/S,如果你用的是削减后概率 p_s,把最后的除以 S 改成乘上概率向量即可。
%% 目标函数:期望运行成本最小 C_fuel = sum(sum(P_MT)) * gas_price; C_buy = sum(sum(P_buy .* Price_buy)); C_sell = sum(sum(-P_sell .* Price_sell)); C_cut = sum(sum(P_load_cut .* cut_compensation)); C_curtail = sum(sum((P_curtail_w + P_curtail_pv) .* penalty_curtail)); obj = (C_fuel + C_buy + C_sell + C_cut + C_curtail) / S; %% 求解 ops = sdpsettings('solver', 'cplex', 'verbose', 2, 'showprogress', 1, ... 'cplex.mip.tolerances.mipgap', 1e-4); diagnosis = optimize(F, obj, ops); if diagnosis.problem == 0 disp('求解成功'); else disp(['求解状态:', yalmiperror(diagnosis.problem)]); end目标函数用 sum(sum(...)) 把 S 个场景的总成本累加,再除以 S 得到期望值。mipgap 设成 1e-4 在我接触的算例里足够严谨,再小只会徒增求解时间,对调度曲线肉眼不可见。如果 diagnosis.problem 返回非 0,优先检查约束集合里变量的维度是否匹配,这是出错率最高的地方。另外注意 C_sell 前面带了负号,因为售电是收益,在成本最小化目标里应当作为负项存在,如果把符号写反,程序很可能干脆不售电,结果曲线会出现诡异的高峰购电。
3.5 结果输出:还原调度计划与关键指标
求解完成后,把每个场景的功率取平均,作为理论上最可预期的运行曲线来展示:
P_MT_avg = mean(P_MT, 2); P_ess_avg = mean(P_dis - P_ch, 2); P_buy_avg = mean(P_buy - P_sell, 2); P_wind_avg = mean(Pt_wind + Pt_pv - P_curtail_w - P_curtail_pv, 2); P_cut_avg = mean(P_load_cut, 2); %% 总成本与弃风弃光率 total_cost = value(obj); wind_total = sum(sum(Pt_wind)); curtail_total = sum(sum(P_curtail_w + P_curtail_pv)); curtail_rate = curtail_total / (wind_total + 1e-6); figure; plot(1:T, value(P_MT_avg), 'LineWidth', 1.5); hold on; plot(1:T, value(P_ess_avg), 'LineWidth', 1.5); plot(1:T, value(P_buy_avg), 'LineWidth', 1.5); plot(1:T, value(P_wind_avg), 'LineWidth', 1.5); legend('MT出力','储能净放电','电网净购电','风光消纳','Location','best'); xlabel('时段/h'); ylabel('功率/kW');这里取平均只是一种展示手段,注意不要把平均后的功率当成调度指令。随机优化的真实输出是一个决策表:u_MT 和购售电状态是日前指令,P_MT 和储能功率是每个场景下的预案。微网实际运行到第二天时,把最接近当前天气的场景预案拿出来作为参考,再结合实时滚动修正。如果论文里画的是多条阴影带曲线,那多半是把 S 个场景的功率都画出来了,你可以直接 plot(1:T, value(P_MT)) 得到所有场景的曲线合集,这个阴影带本身就是「不确定性」最直观的呈现方式。
4. 复现随机优化调度绕不开的避坑点:为什么你的结果和论文对不上
4.1 场景数一味增加,求解器卡死或内存溢出
现象:把削减后场景数从 10 改成 50,程序跑了一个多小时还没出结果,任务管理器里内存占用接近系统上限,最后报迭代上限或内存不足。
原因:每个场景都会把功率平衡、爬坡、储能 SOC 这套约束复制一遍,存储与求解开销随场景数线性甚至超线性增长。加上 u_ess_charge 这类场景相关二进制变量,混合整数规划的整数变量数量直接翻倍。场景数冲到 50 时,整数变量数量过千,CPLEX 的分支定界过程会非常痛苦。
解决:在工程算例里,削减后场景数取 10 到 20 足够稳定。如果要对论文里的某个极端场景单独讨论,再单独提高该场景的权重,而不是整体加场景数。另一个实用手段是让储能充放电状态与场景无关,只让功率大小随场景波动,模型规模立刻下降一大截,代价是储能充放电模式不能再随场景翻转,这在大多数日前调度问题里是可接受的工程折中。
4.2 变量全部场景相关,算出一个不可执行的「假最优」
现象:求出来的总成本比论文低很多,但把各场景的功率曲线拿出来看,同一时段燃气轮机启停状态在不同场景下不一样,部分场景开启、部分场景关闭。程序运行没有警告,结果看起来也很漂亮。
原因:变量选定出了问题。P_MT 是连续变量可以场景相关,但 u_MT 是第一阶段决策,必须跨场景保持一致。如果把启停状态也写成 S 列,等于允许调度员在场景揭晓后再决定开不开机,这在日前时间尺度上不成立,目标函数当然更便宜。
解决:检查所有二进制变量,凡是代表日前的开停机、购售电状态、设备投切状态,一律定义成 T×1 维度,与场景无关。只有功率大小、充放电量这类执行层变量才允许 T×S。程序改完后,目标函数会比之前略高,这才是符合两阶段随机优化语义的正确结果,也是论文里真实成本通常高于你第一次跑出来的原因所在。
4.3 风电场景里出现超出装机容量的功率值
现象:场景生成脚本跑完,查看 wind_scen 时发现某些行最大值超过风电场额定装机容量,甚至出现轻微负值。
原因:用正态分布近似风速或功率预测误差时没有做截断,随机采样总会生成物理上不可能的值。普通正态分布左边没有边界,小概率采到负风速,映射到功率曲线后就是一个负的风电出力,直接进功率平衡约束会严重扭曲调度结果。
解决:对采样分布做截断处理。风功率采样后在 [0, 额定装机容量] 区间内做投影;负荷场景同样在合理区间内截断。更严谨的做法是直接用实测历史误差的核密度估计来采样。这个坑很难从求解状态里发现,因为模型会把负出力当作反向负荷一样消化掉,所以必须在场景生成脚本里写断言,每段采样后检查物理边界。
4.4 多日连续调度时储能终值约束导致无解
现象:单独跑某一天没问题;把程序连跑七天,每一天都是独立初始化,结果某些天的场景里负荷很低、风电很大,却仍然必须把 SOC 在一天末拉回 0.5,目标函数畸高,甚至报无解。
原因:SOC_end 固定约束太强。现实中连续调度时一天的储能终值不一定要等于初值,只要在多日尺度上满足可持续约束即可。程序里把每天初值设为 0.5 又把终值强制为 0.5,遇到源荷极端不平衡的算例就会被迫让储能做无意义的搬运。
解决:把 SOC_end 从等式改成范围约束,比如允许在 [0.4, 0.6] 之间,或改成相邻两日在整点交界处 SOC 连续。我在做一周连算时,通常第一步先解一次确定性调度获取自然终值,然后把第二天的初值直接设成前一天的自然终值,这样既不破坏可行性,也保留了储能的跨日调节潜力。
5. 从程序到论文的验证路线:如何确认你的复现没有写错
5.1 基态场景对照法:先跑确定性模型再跑随机模型
拿到程序后不要急着调场景数。第一步,把所有场景取平均,把平均后的风电、光伏、负荷作为单一确定性输入,跑一遍同一边约束结构,得到确定性调度结果。第二步,把随机模型跑出来,对比两者的总成本和出力曲线。随机模型的总成本必须高于或等于确定性模型,因为不确定性要付出备用成本,这是随机优化的理论边界。
如果随机模型成本低于确定性模型,几乎可以断定你的场景相关变量出了问题,比如负荷削减惩罚设得过低,或变量被错误定义成了场景相关。这个对照关系和具体数据无关,是判断复现程序是否正确的最快方法。我在调程序时把这步放在最前面,省掉了后面大量查维度、查约束对错的精力。
5.2 关键参数敏感性分析表
复现时必做的一张表是场景数与 mipgap 的敏感性分析。场景数从 5、10、20、30 逐个跑一轮,记录求解时间、总期望成本、弃风弃光率。结果应该呈现这样的规律:场景数少时成本偏高或偏低不稳定,到 20 左右趋于平稳,再增加场景数成本变化很小但求解时间指数上涨。如果观察到场景数从 10 到 30 成本一直单调大幅变化,说明场景生成阶段可能没覆盖足够的极端情况,或者削减算法把典型场景削丢了。
5.3 与论文数值对不上时的检查清单
论文给的结果通常凝聚了几百行代码和几组特定参数。复现对不上时,我按这张清单逐项排查:
| 检查项 | 常见错误 |
|---|---|
| 功率单位 | 论文用 MW,程序用了 kW,成本数量级差 1000 |
| 价格单位 | 元/MWh 与元/kWh 混用 |
| 时间颗粒度 | 15 分钟数据按 1 小时计,24 时段变 96 时段 |
| 负荷参数 | 可调负荷比例取错,论文 15% 程序 5% |
| 储能容量 | 忘记除以 Eess_max,SOC 公式直接加电量 |
| 场景概率 | 削减后场景概率没有归一化 |
经验告诉我,一个程序数字对不上,十有八九是单位或归一化问题。将目标函数拆开,用两个场景手算一次期望成本,就能快速定位是哪一项差异最大。论文里的总成本如果包含启停成本而你的模型没写,这一项单独对不出来也正常,先核对分项构成再整体对齐。
5.4 可视化检查:看曲线是否符合物理直觉
画最终出力曲线时,至少要满足几条物理直觉:燃气轮机不频繁启停,启停次数在一天内不超过 2 到 3 次;储能充电和放电不会出现在同一时段,从曲线看不会同时有正负值;夜间风电大发而负荷低的时候,储能充电或向电网售电的功率段明显;光伏中午高峰时出力跟着抬升。任何一条不满足,都应回到约束里去检查,而不是急于调参数压成本。这类程序的黑匣子感很强,但曲线本身会告诉你模型内部发生了什么。
6. 进阶技巧:从固定场景随机优化平滑升级到分布鲁棒优化
场景法随机优化最被人诟病的点,是它假设场景集就是真实分布的近似,一旦第二天实际出现的源荷偏离了场景集,调度方案就没有显式的鲁棒性保证。我常用的进阶做法,是把代码里确定性等价的目标函数改成分布鲁棒形式:给每个典型场景一个名义概率,同时构造以这些概率为中心的 Wasserstein 球,用半径参数控住概率分布的偏差。具体到程序里,目标函数从固定期望成本变成带罚项的 worst-case 形式,这个升级在 CPLEX 里不需要引入新的非线性,只需要对离散场景子集做加权和取最大即可。
我实际落地时,并不对整个系统做全程鲁棒,而是只对弃风弃光和切负荷两个惩罚项做分布鲁棒化,因为这两项是场景偏差最敏感的成本来源。改动量很小,但得到的成本曲线会比纯随机优化更保守,也更容易解释给评审听。如果你对这个方向感兴趣,可以沿着「随机优化 → 分布鲁棒优化」这个顺序迭代,而不是直接上手复杂算法,这样每次改动都能对照程序结果检查是否进步。
做完这一版后,我习惯在程序里保留一个开关:是跑纯随机优化、还是跑分布鲁棒优化。两个结果放一起对比,正好能串起论文里的「不确定性成本」叙事,也让程序的价值从复现变为延伸。如果只想把论文复现完就收工,那也至少要把场景削减的随机种子固定,这是我在反复写报告里学到的习惯,不然每跑一次结果都不一样,后面任何数字对比都站不住脚。希望这些经验帮你在复现与拓展这套虚拟电厂微网调度程序时少走几步弯路。
本文还有配套的精品资源,点击获取