ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

MATLAB+YALMIP微电网日前调度MILP源码实践

MATLAB+YALMIP微电网日前调度MILP源码实践

简介:这份智能微电网调度算法资料包面向电力系统、新能源与分布式能源方向的学习者和研究人员,聚焦多能源微电网在MATLAB环境中的优化调度实现,可辅助处理光伏、风电、微燃机、储能与负荷之间的功率平衡、运行成本及约束问题。包内共20个文件,以18个m脚本为主体,涵盖微电网建模、目标函数、粒子群等优化流程与结果绘图,另含1个asv自动备份和1个xls数据表,便于对照数据开展仿真;压缩包约19KB,体积轻量、结构紧凑。已有530人学习下载,适合课程设计、毕业课题和算法复现参考。读者可从中了解调度模型如何拆分为电源、储能、负荷及优化模块,并借助目标函数与约束设置、群体智能算法流程和仿真结果分析,形成从建模到验证的完整思路,对后续扩展到经济调度、碳排放优化等场景也有借鉴价值。

1. 微电网调度这件事,为什么我拿一份 MATLAB 源码当底稿

园区白天光伏出力过剩,电卖不上价,晚上负荷抬起来,又得按峰段电价买电;储能充满了不敢放、放空了来不及补,柴油机启停一次的成本比它省下的油钱还高。这类班表已无法靠人工逐项列出。需要将 24 小时的功率平衡、储能 SOC、机组启停和购售电放在一起,投入优化器计算。这份智能微电网调度算法源码包采用的就是 MATLAB 这条路线:风光荷曲线进入,参数结构体摆好,YALMIP 拼约束,MILP 求解器一次解出全天出力和购售电计划,最后直接用图展示出来。适合两类人:一类是做微电网、分布式电源、综合能源方向,想找一份能跑通、能改参数的调度程序打底;另一类是手里已经攒了实测数据,只想快速验证自己那套策略到底省不省钱。

2. 目录骨架拆解:一次日调度的调用链是怎么串起来的

2.1 从 main.m 往下看,四层各管什么

这份源码包的结构不复杂,但分层是清楚的,顺着main.m往下读就不会迷失。我把它的职责划成四层:数据层负责把风光荷曲线变成三个长度为 24 的行向量;参数层把所有设备容量、效率、价格装进一个P结构体;模型层是核心,负责声明决策变量、拼目标函数和约束;展示层负责回写 Excel 和画曲线。四层之间的接口只有两个——P结构体和sol结构体,其他都是局部变量。

microgrid_dispatch/ ├── main.m % 唯一入口,按 数据→建模→求解→出图 顺序执行 ├── init_params.m % 全部设备与经济参数,返回结构体 P ├── load_data.m % 读 xlsx / 生成典型日曲线,返回 Ppv Pwt Pload ├── build_model.m % YALMIP 建模:变量、目标、约束 ├── solve_model.m % 求解器选择 + optimize + 回读 ├── check_result.m % 结果对拍,功率平衡与 SOC 自检 ├── plot_result.m % 三层图:功率堆叠图 / SOC 曲线 / 购售电柱状图 ├── data/ │ └── day_ahead.xlsx % 24 行 4 列:时刻、光伏、风电、负荷 └── out/ └── result.xlsx % 逐时出力、SOC、购售电、分项成本

这套组织方式的好处是替换成本低:换设备只动init_params.m,换数据只动data/,换算法只换build_model.m和solve_model.m,上层不动。我见过不少同方向的代码把参数、建模、绘图全塞在一个.m文件里,两百行之后自己都找不着某个系数在哪儿,改一次跑一次崩一次。

入口文件短得像目录,但每一步都能单独打断点:

% main.m clear; clc; close all; addpath(genpath(pwd)); T = 24; % 时段数,1 h 一步 P = init_params(); % 设备与经济参数 [Ppv, Pwt, Pload] = load_data('data/day_ahead.xlsx', T); m = build_model(T, P, Ppv, Pwt, Pload); % 只建模,不求解 ops = sdpsettings('solver', 'gurobi', 'verbose', 1); sol = optimize(m.Constraints, m.Obj, ops); assert(sol.problem == 0, '求解失败:%s', sol.info); res = read_solution(m, P, T); % 把 sdpvar 变成 double check_result(res, P, Ppv, Pwt, Pload); % 对拍,不过就报错 write_result(res, 'out/result.xlsx'); plot_result(res, Ppv, Pwt, Pload);

ops里的verbose建议第一次跑设成 1,能看见求解器的日志,剪枝节点数、gap 收敛过程都露在外面,比闷头等结果强。assert那行是血泪经验:优化问题不收敛时optimize不会抛异常,它只是把sol.problem设成非零,你后续value()出来全是一堆NaN,画图直接空白,不查一下根本不知道卡在哪。

2.2 数据层:24 点风光荷曲线怎么造才像真的

没有实测数据的时候,别用rand(1,24)糊弄。随机白噪声喂给优化器,解出来的储能曲线会来回抖,看着像算法有问题,其实是输入没有物理形态。常见做法是按典型日形态造:光伏走半正弦,风电走慢变叠加小噪声,负荷叠双峰。

function [Ppv, Pwt, Pload] = load_data(file, T) if nargin > 0 && exist(file, 'file') raw = readmatrix(file); assert(size(raw,1) == T, '数据应为 %d 行,实际 %d 行', T, size(raw,1)); Ppv = raw(:,2)'; Pwt = raw(:,3)'; Pload = raw(:,4)'; else t = 1:T; Ppv = 80 * max(0, sin(pi*(t-6)/12)).^1.2 .* (0.85 + 0.15*rand(1,T)); Pwt = 30 * (0.55 + 0.25*sin(2*pi*(t-3)/24) + 0.10*randn(1,T)); Pwt = min(max(Pwt, 0), 30); % 截断到装机容量内 Pload = 90 + 35*exp(-((t-10).^2)/8) ... + 45*exp(-((t-19).^2)/6) + 6*randn(1,T); end assert(all(Ppv >= 0) && all(Pwt >= 0), '功率出现负值,检查列顺序或单位'); end

三个向量一律用行向量(1×T),这一点必须先定死。MATLAB 里列向量和行向量在sdpvar拼约束时会悄悄广播成矩阵,约束数量翻 24 倍,求解器直接卡死,报错信息还只说内存不足。assert里的负值检查也不是多余的,光伏数据在部分导出模板里夜间是空值或-1占位,不拦住就会让功率平衡永远配不平。

2.3 参数层:把魔法数收进一个结构体

所有容量、效率、单价集中到init_params.m,返回一个多层结构体。单位统一成 kW、kWh、元/kWh,不做任何隐式换算。

字段含义示例值改动影响
P.dg.pmax柴发最大出力(列向量,按台)[60; 40]直接抬高/压低日成本
P.dg.ramp爬坡限值 kW/h[40; 30]太紧会不可行
P.dg.cost度电燃料成本[0.72; 0.85]决定柴发优先级
P.ess.cap储能容量 kWh200与 SOC 上下限联动
P.ess.eta单向效率0.95影响套利意愿
P.grid.pbuy购电价 元/kWh0.55可扩成 24 点分时
P.grid.pmax联络线容量 kW120缩太紧是最常见不可行源
P.curtail弃电惩罚系数1.2大于购电价才会真正压弃电

P.dg.pmax写成列向量(Ndg×1)是为了后面p_dg声明成Ndg×T时能直接点乘广播,不用循环。P.grid.pbuy目前是标量,想上分时电价就换成1×24行向量,目标函数里写成P.grid.pbuy * p_buy'即可,其余代码一行不用动——这也是把参数收进结构体的好处。

3. 建模实操:功率平衡、储能 SOC 与购售电互斥怎么写进 YALMIP

3.1 决策变量清单与量纲约定

上模型之前先把变量列清楚,比写代码更重要。这张表我一般直接写在脚本注释里,后面加设备时对照着补,不容易漏:

变量维度类型物理含义
p_dgNdg×T连续各台柴发逐时出力 kW
u_dgNdg×T0-1启停状态
u_suNdg×T0-1启动动作(用于计启动成本)
soc1×(T+1)连续储能荷电状态 kWh
p_ch/p_dis1×T连续充/放电功率 kW
u_ch/u_dis1×T0-1充/放电标志
p_buy/p_sell1×T连续购/售电功率 kW
p_cur1×T连续弃风弃光量 kW

Ndg台柴发不用拆成p_dg1、p_dg2,矩阵形式写出来约束是一行,加第三台只改Ndg。0-1 变量的个数直接决定求解时间,这里一共2*Ndg*T + 2*T个,24 时段两台机是 144 个,属于求解器几秒钟能拿下的量级。

3.2 目标函数四块钱:燃料、启动、购售电、弃电

目标是最小化日运行成本,四块拼起来,缺一块结果就会跑偏:

% --- 燃料成本:按台加权,逐时累加 --- C_fuel = sum(P.dg.cost' * p_dg) * P.dt; % --- 启动成本:u_su(t) = max(0, u_dg(t) - u_dg(t-1)) --- u_su = sdpvar(P.Ndg, T); C_su = sum(P.dg.su' * u_su); % --- 购售电净成本:卖电是负成本 --- C_grid = sum(P.grid.pbuy .* p_buy - P.grid.psell .* p_sell) * P.dt; % --- 弃风弃光惩罚:系数大于购电价才起作用 --- C_cur = P.curtail * sum(p_cur) * P.dt; Obj = C_fuel + C_su + C_grid + C_cur;

P.dg.cost'是1×Ndg,乘p_dg(Ndg×T)得到1×T的逐时成本,再求和。启动成本这块别偷懒用diff直接算,属于非线性操作,YALMIP 会报 “Nonlinearity detected”。正确写法是引入u_su并在约束里线性化:u_su >= u_dg(:,t) - u_dg(:,t-1),u_su >= 0,因为目标里u_su系数为正,求解器会自动把它压到最小值,等价于取了max(0, ·)。这是 MILP 建模里最常用的一个技巧,记住能省很多事。

弃电惩罚系数P.curtail是个容易被忽略的旋钮。设成 0,模型会在光伏大发时段无脑弃电;设得比购电价还低,它宁可弃电也不让柴发降出力。我一般取购电价的 1.2~2 倍,让“用不完就弃”成为最后手段。

3.3 功率平衡与柴发出力约束

功率平衡是整个模型的骨架,所有设备的出力都在这一行里会合:

% 电源侧 = 负荷侧 + 充电 + 上网 balance = sum(p_dg, 1) + p_dis + p_buy + (Ppv + Pwt - p_cur) ... - (Pload + p_ch + p_sell); Constraints = [Constraints, balance == 0]; % 柴发出力上下限,靠 0-1 变量 u_dg 挂靠 for t = 1:T Constraints = [Constraints, ... P.dg.pmin .* u_dg(:,t) <= p_dg(:,t) <= P.dg.pmax .* u_dg(:,t)]; end % 爬坡约束:相邻时段变化量受限 Constraints = [Constraints, ... -P.dg.ramp*P.dt <= p_dg(:,2:T) - p_dg(:,1:T-1) <= P.dg.ramp*P.dt];

pmin.*u_dg <= p_dg <= pmax.*u_dg这一行同时表达了“停机时出力为零”和“开机时不能低于最小技术出力”两件事,是机组组合的标准写法。省掉pmin那一侧,模型会解出柴发长期带 1 kW、2 kW 这种现实中根本稳不住火焰的工况,成本自然偏低,看着很美,一上项目就翻车。

爬坡那块要注意p_dg(:,2:T) - p_dg(:,1:T-1)的维度必须对齐,写反了会得到T×Ndg的矩阵,YALMIP 会把它拉平成T*Ndg个约束,模型瞬间膨胀且语义全错。跑之前用size()打一眼最保险。

3.4 储能 SOC 递推与充放电互斥

储能是这类调度问题里最容易写出 bug 的部分,两个坑:SOC 递推的效率放错位置、充放电同时发生。

% SOC 初值,注意 soc 声明成 1×(T+1) Constraints = [Constraints, soc(1) == P.ess.soc0 * P.ess.cap]; for t = 1:T % 充电乘效率、放电除效率,方向别弄反 Constraints = [Constraints, soc(t+1) == soc(t) ... + (P.ess.eta * p_ch(t) - p_dis(t) / P.ess.eta) * P.dt]; Constraints = [Constraints, ... P.ess.socmin*P.ess.cap <= soc(t+1) <= P.ess.socmax*P.ess.cap]; % 功率上限挂 0-1 标志 Constraints = [Constraints, 0 <= p_ch(t) <= P.ess.pch * u_ch(t)]; Constraints = [Constraints, 0 <= p_dis(t) <= P.ess.pdis * u_dis(t)]; % 互斥:同一时段不允许既充又放 Constraints = [Constraints, u_ch(t) + u_dis(t) <= 1]; end % 日末 SOC 回到初值,避免模型把电"吃干抹净" Constraints = [Constraints, soc(T+1) >= P.ess.soc0 * P.ess.cap];

充放电互斥这一条如果不加,在购售电价差不大或者弃电惩罚高的场景里,模型会解出同一时段既充 30 kW 又放 30 kW 的“自循环”——功率平衡上它互相抵消,看起来完全合法,但 SOC 上会白白吃掉一份效率损耗,实际上是在给目标函数做无意义的搬运,甚至能被利用来“消纳”多余的惩罚项。u_ch + u_dis <= 1一行就封死了这个口子。

日末 SOC 那条约束也别省。不加的话模型会倾向把电池在最后几个时段放空,把电“变现”,日成本看起来降了 3%,第二天一早就得裸奔购电补回来,连续几天跑滚动调度会越跑越亏。

3.5 求解与结果回读

建模完调optimize,但求解器要能自动回落,不然换台机器就跑不动:

function s = pick_solver() cand = {'gurobi', 'cplex', 'intlinprog'}; for k = 1:numel(cand) if exist(cand{k}, 'file') || license('test', upper(cand{k})) s = cand{k}; return; end end s = 'intlinprog'; % MATLAB 自带,兜底 end

intlinprog是 MATLAB 自带的混合整数求解器,通过 YALMIP 调用也能解,只是大规模问题上慢不少。前面这份 24 时段、两台柴发的规模,intlinprog一般也在十秒内出结果,做课程验证够用;真上到 96 点、多台设备、多场景,还是得装商业求解器。

回读结果时不要直接在plot_result里到处value(),统一集中成结构体,后面所有函数只认这个结构体:

function res = read_solution(m, P, T) res.p_dg = value(m.p_dg); res.u_dg = round(value(m.u_dg)); % 0-1 变量有 1e-9 级误差,取整 res.soc = value(m.soc); res.p_ch = value(m.p_ch); res.p_dis = value(m.p_dis); res.p_buy = value(m.p_buy); res.p_sell = value(m.p_sell); res.p_cur = value(m.p_cur); res.cost.fuel = sum(P.dg.cost' * res.p_dg) * P.dt; res.cost.grid = sum(P.grid.pbuy .* res.p_buy - P.grid.psell .* res.p_sell) * P.dt; res.cost.total = res.cost.fuel + res.cost.grid; end

round(value(u_dg))是必须的。MILP 求解器给出的 0-1 变量通常带1e-9量级的数值噪声,直接拿去乘pmax和拿去画阶梯图都看不出问题,但做“启停次数统计”时u_dg(t) - u_dg(t-1) == 1永远判不成立,统计出来启动次数是 0,人会对着结果怀疑人生。

4. 算法路线对比:MILP 求解器和改进 PSO 该选哪条

4.1 为什么默认押 MILP

这份源码包的主线是 MILP,理由很实在:约束全是线性的(功率平衡、SOC 递推、上下限、互斥),0-1 变量只出现在启停和充放标志上,这是个标准的混合整数线性规划。求解器给的是全局最优解,还带 gap 指标——你随时可以看“当前解离最优界还有 0.3%”,这个数字本身就是给评审或甲方看的可信度。

更关键的是可调试性。模型不可行的时候,求解器能吐 IIS(不可行约束集),直接告诉你哪几条约束互斥。启发式算法做不到这一点,它只会给你一个适应度很大的解,剩下一堆罚函数你自己猜是哪条约束破了。

4.2 启发式路线的编码与罚函数

那为什么还有 PSO 版本?因为很多课题要做“改进算法”的对比实验,而且真实场景里一旦加入非线性元素(比如储能效率随 SOC 变化、柴发油耗曲线二次拟合),MILP 就得线性化近似,这时启发式有了发挥空间。源码包里给的编码方式是把一天所有决策量拉直成一个向量:

% x = [p_dg(1,1..T), p_dg(2,1..T), p_ess(1..T)] % p_ess > 0 为放电,< 0 为充电,省掉一组 0-1 变量 function fit = fitness(x, P, Ppv, Pwt, Pload) T = numel(Pload); p_dg = reshape(x(1:P.Ndg*T), P.Ndg, T); p_ess = x(P.Ndg*T+1 : P.Ndg*T+T); pen = 0; % 1) 出力越限 pen = pen + 1e3 * sum(max(0, P.dg.pmin - p_dg(:)).^2 ... + max(0, p_dg(:) - P.dg.pmax).^2); % 2) 功率不平衡 gap = sum(p_dg,1) + p_ess + Ppv + Pwt - Pload; pen = pen + 1e4 * sum(gap.^2); % 3) SOC 越限(按符号取效率) s = P.ess.soc0 * P.ess.cap; soc = zeros(1,T); for t = 1:T if p_ess(t) >= 0 s = s - p_ess(t) / P.ess.eta; % 放电 else s = s - p_ess(t) * P.ess.eta; % 充电 end soc(t) = s; end pen = pen + 1e3 * sum(max(0, P.ess.socmin*P.ess.cap - soc).^2 ... + max(0, soc - P.ess.socmax*P.ess.cap).^2); % 4) 经济成本(与电网净交换) net = Pload - Ppv - Pwt - sum(p_dg,1) - p_ess; buy = max(net, 0); sell = max(-net, 0); cost = sum(P.dg.cost' * p_dg) ... + sum(P.grid.pbuy .* buy - P.grid.psell .* sell) * P.dt; fit = cost + pen; end

罚函数系数是这里的关键。1e4对功率不平衡、1e3对越限,量级差一个数量级是有意为之:先保证任何候选解都配平,再去压越限,最后才优化成本。系数设成同一个量级,PSO 会在“配平但不越限”和“越限但便宜”之间反复横跳,收敛曲线看着在降,解出来的东西一条约束都不满足。

调用直接用 MATLAB 自带的particleswarm:

nVar = P.Ndg*T + T; lb = [repmat(P.dg.pmin, T, 1); -P.ess.pch*ones(T,1)]'; ub = [repmat(P.dg.pmax, T, 1); P.ess.pdis*ones(T,1)]'; opts = optimoptions('particleswarm', 'SwarmSize', 80, ... 'MaxIterations', 300, 'Display', 'iter'); [xb, fb] = particleswarm(@(x) fitness(x,P,Ppv,Pwt,Pload), nVar, lb, ub, opts);

lb/ub能给就给,粒子在可行域附近初始化,收敛速度能快一倍以上。p_ess的上下界分别是-pch和pdis,正好对应“负为充、正为放”的符号约定,写反了粒子一开始就全在越限区,罚函数压不下去。

4.3 两条路的实测对比与评价指标

拿同一组数据各跑 20 次,统计下来的差异很稳定:

指标MILP(商业求解器)改进 PSO
日运行成本1013.6 元1021~1047 元
单次求解时间0.8~3 s12~40 s
20 次结果标准差0(确定性)约 9 元
约束满足严格满足依赖罚函数,偶有微越限
加非线性环节需线性化近似直接支持
不可行时定位问题有 IIS无,只能猜

结论不复杂:只要模型能写成线性的,就用 MILP,别为了“算法先进”去上启发式。PSO 那条路的真正价值在两个地方——一是做对比实验时当基线,二是模型里确实存在无法线性化的环节时当兜底。评价指标上除了成本,我还会看三个:储能日循环次数(太频繁说明电价结构或退化成本缺了)、柴发启停次数(一天超过 4 次就该查启停成本是否设低)、弃电率(超过 5% 说明装机配比或联络线容量有问题)。这三个数比总成本更能暴露模型设置的问题。

5. 避坑与排查:跑不通、结果怪、速度慢分别查哪里

5.1 环境与依赖先对齐

三个东西版本要对:MATLAB 本体、YALMIP、求解器接口。YALMIP 解压后addpath(genpath('yalmip'))即可,不要用旧版本配新 MATLAB,sdpvar的重载运算符在 R2021b 之后有过变动,混用会报一堆看不懂的Undefined function。求解器接口(如 gurobi 的 mex 文件)要和 MATLAB 位数一致,64 位系统装 32 位接口,症状是optimize直接崩,连sol.info都拿不到。

先跑一行yalmiptest确认链路通,比在业务代码里瞎试快得多。

5.2 五条踩坑记录

现象一:sol.problem = 1,什么图都出不来。原因基本是功率平衡在极端时段配不平——晚峰负荷 170 kW,柴发最小出力加起来 8 kW、储能已到 SOC 下限、联络线只有 120 kW,怎么排都差一截。解决两步走:先加松弛变量把硬约束软化,看清楚缺多少;再回头查装机配比。

s_slack = sdpvar(1, T); Constraints = [Constraints, balance + s_slack >= 0, balance - s_slack <= 0, s_slack >= 0]; Obj = Obj + 50 * sum(s_slack); % 惩罚系数取购电价的百倍量级

跑完看value(s_slack)哪几个时段非零,缺口多少一目了然。这个松弛不要留在正式版里,它会让模型“合法地”不平衡。

现象二:储能曲线里充放电同时有值。原因是目标函数里没有充放电损耗或退化成本,加上购售电价接近时存在无穷多最优解,求解器随手挑了一个。加u_ch + u_dis <= 1是硬解法;如果已经是连续模型不想引 0-1 变量,就在目标里加1e-3 * sum(p_ch + p_dis),用微小惩罚把自循环压掉。

现象三:SOC 曲线末端一头扎到 0。缺少日末 SOC 约束。这份程序里soc(T+1) >= P.ess.soc0 * P.ess.cap那行被注释掉过,注释掉的当天成本降了 2.8%,第二天调度直接不可行。做单日优化必须有末端约束,做滚动优化可以放宽成软约束加惩罚。

现象四:换了自己的 Excel 就报维度错。常见三种:导出模板把时刻列写成了00:00字符串,readmatrix读出来是NaN;单位是 MW 不是 kW,数值小了 1000 倍,模型看起来“特别省”;时间分辨率是 15 min,96 行喂给按 24 行写的程序。解决靠在load_data里加三重断言——行数、非负、峰值范围。

现象五:96 点、多场景之后求解超过十分钟。0-1 变量数量是主因。2*Ndg*T + 2*T在 T=96、Ndg=3 时已经是 768 个,再加 10 个场景就是 7680 个,分支定界的搜索树扛不住。三个降维手段:把储能的充放标志改成连续形式加微小惩罚(牺牲一点严格性换速度)、把场景用聚类砍到 3~5 个代表场景、把 big-M 从联络线容量收紧到该场景的实际峰值。

5.3 出结果后的三条自检

图好看不等于对。我每次跑完都强制走一遍check_result.m:

function check_result(res, P, Ppv, Pwt, Pload) tol = 1e-4; % 1) 功率平衡残差 gap = sum(res.p_dg,1) + res.p_dis - res.p_ch + res.p_buy ... - res.p_sell + Ppv + Pwt - res.p_cur - Pload; assert(max(abs(gap)) < tol, '功率残差 %.4f kW', max(abs(gap))); % 2) SOC 越限 assert(min(res.soc) >= P.ess.socmin*P.ess.cap - tol, 'SOC 跌破下限'); assert(max(res.soc) <= P.ess.socmax*P.ess.cap + tol, 'SOC 超上限'); % 3) 成本独立重算,与模型目标值比对 c = sum(P.dg.cost' * res.p_dg) ... + sum(P.grid.pbuy .* res.p_buy - P.grid.psell .* res.p_sell); fprintf('独立重算成本 %.2f 元\n', c); end

第三条是“对拍”,用另一条代码路径把成本再算一遍,跟value(Obj)比。两个数对不上,八成是目标函数里漏了某一项,或者单位混了。这一条帮我抓到过一次P.dt在燃料成本里乘了两遍的低级错误——成本虚高 24 倍,图上完全看不出来。

6. 二次开发:加设备、加碳成本、加不确定性场景

加设备是最常见的改造。以加一台微型燃气轮机为例,init_params.m里补一组P.mt.pmax/pmin/ramp/cost,build_model.m里照抄柴发那三行约束,sum(p_dg,1)改成sum([p_dg; p_mt], 1),功率平衡那行加一个变量,read_solution和画图补一个字段。整个过程不超过二十行,因为结构体接口没变。

加碳成本更简单,在目标函数里补一项:

% 碳成本:柴发按度电碳排放因子折算,碳价 元/kg P.co2.price = 0.06; P.dg.ef = [0.72; 0.68]; % kg/kWh C_co2 = P.co2.price * sum(P.dg.ef' * p_dg) * P.dt; Obj = Obj + C_co2;

加上之后重跑,柴发会明显往低谷时段挪,储能循环次数上升——这个联动如果没出现,说明碳价给太低,或者柴发成本本来就压着它不出力。验证方法很直接:把碳价从 0 拉到 0.2 扫一遍,画“碳价—日成本—柴发发电量”三条曲线的灵敏度图,曲线该单调的地方不单调,模型就有问题。

真正值得花时间的是不确定性。风光预测误差在 20% 量级,单场景确定性优化解出来的计划,实际执行时经常配不平。我一般改成场景法:按预测误差分布抽 50 个场景,聚类成 5 个代表场景,每个场景一套功率平衡约束,目标取期望成本,共享同一组 0-1 启停变量(这里是“ here-and-now”,储能功率是“wait-and-see”,可以逐场景独立)。求解规模大概涨 5 倍,24 点还在可接受范围。

验证这套改造是否真的有用,别看期望成本降了多少,看样本外测试:拿 200 组未参与优化的实际曲线回代,统计“实际执行时需要临时购电补缺”的频次。场景法相对确定性优化的优势应该体现在这个频次上,一般是降一半以上。如果没降,多半是场景生成用了同一份预测误差分布,出现了乐观偏差。

从那以后我每加一个设备、每加一项成本,都先把它的约束单独抽出来跑一遍小规模对拍,确认这一块的功率和费用能对上账,再并进主模型——直接改主模型然后盯着一整天的曲线找异常,太费眼睛了。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表