☰
微电网日前调度优化实战:MATLAB与CPLEX求解MILP模型
2026/9/30 8:38:46 网站建设 项目流程

微电网调度优化这几个字,听起来像是研究生论文里的术语,但落地到实际工程中,无非就是回答一个问题:未来24小时,光伏、风电、储能、柴油机和主网各自该出多少力,才能在满足负荷的同时把总运行成本压到最低。我最近用 MATLAB 与 CPLEX 把这套流程从头到尾跑了一遍,从模型搭建、约束矩阵构造、求解器调试到结果曲线分析,踩了不少坑,也慢慢摸出一套可复用的实操路径。这篇文章就围绕“微电网日前调度优化 + MATLAB 调用 CPLEX 求解 MILP”这个核心展开,既讲清楚为什么要这么配,也把关键代码和调试经验摊开来说。如果你正在做微电网经济调度、储能充放电策略优化,或者刚入门想找一套能直接上手的求解框架,都可以把这些内容当作参考。尤其适合那些模型公式能看懂、但一写代码就卡住的朋友,其实从公式到稀疏矩阵再到求解器,中间只差一层“翻译”工作。

1. 项目背景与问题拆解

1.1 微电网调度到底在优化什么?

微电网是一个小范围发输配用系统,常见组成包括光伏、风机、储能电池、柴油发电机,以及本地负荷和与主网的公共连接点。所谓“调度”,就是调度中心根据预测的负荷曲线、光伏出力曲线和风电出力曲线,提前决定每一个控制时段里各设备应该工作在什么状态。这里的控制时段通常取1小时,也有取15分钟做日内滚动的,但前调度以24小时为周期最常见。

这个问题的数学本质是混合整数线性规划(MILP)。为什么不是简单的线性规划?因为柴油机有“开机/停机”两种离散状态,储能也有“充电/放电”两种互斥状态,这些天然就是0/1变量。如果只用连续变量建模,求解器很可能会给出一个看似合理、实际无法落地的方案,比如柴油机在最小技术出力以下运行,或者储能同一时段一边充电一边放电来赚价差。引入0/1变量之后,模型才真正贴近物理约束。

我习惯用一个类比来理解这件事:微电网调度就像餐厅排班经理。柴油机是正式员工,一开机就是4小时班,不能随时甩手走人;储能是兼职小时工,有最大工时限制,而且不能一边干活一边休息。排班经理要在成本最低的前提下,把每个时段的任务分配清楚。这个排班问题,就是MILP。

1.2 为什么偏偏是 MATLAB 和 CPLEX 的组合?

我第一次做这类优化时,先试了MATLAB自带的intlinprog,小规模问题跑起来还挺顺畅,但一旦24时段全部加上启停变量、储能互斥约束和SOC递推,计算时间就开始失控。后来换成CPLEX,同样的模型规模,求解速度明显上了一个台阶。

这个组合的“奇妙”之处在于:MATLAB负责数据输入、矩阵组装、结果可视化,CPLEX负责核心的分支定界、割平面、启发式搜索。CPLEX是商用级MILP求解器,对整数规划的求解效率和稳定性确实不是优化工具箱里那个内置求解器能比的。当然,Gurobi、SCIP等求解器也很强,但CPLEX有个很实用优势是提供Community Edition免费社区版,个人学习和中小规模项目完全够用。

另外,MATLAB里还有YALMIP这类建模工具,能用符号变量写模型,代码确实短。但我自己更推荐直接用原生API写稀疏矩阵。原因有两条:第一,符号变量在大规模模型下解析速度明显下降;第二,矩阵形式更透明,出了问题可以直接查LP文件,逐行核对约束,排错效率高。

1.3 项目整体技术路线

整个项目我拆成四步走:

  1. 数据准备:整理24小时负荷预测、光伏和风电预测、分时电价、设备参数;
  2. 建模:定义连续变量和0/1变量,写出目标函数和全部约束;
  3. 矩阵化:把等式约束和不等式约束转成Aeq、beq、A、b,并组装成稀疏矩阵;
  4. 求解与后处理:调用CPLEX求精确解,再把结果还原成功率曲线和成本曲线。

这四步看起来平淡无奇,但真正容易翻车的都在第3步。约束之间相互耦合,少写一个等式或者符号写反,模型就会无解;多写一个大M约束,解算时间又会暴涨。后面章节我会把每一步的关键细节展开讲。

2. 目标函数与约束条件的完整推演

2.1 目标函数:总运行成本最小化

日前调度最常用的目标函数是全时段总运行成本最小,包含以下几块:

  • 柴油机燃料成本;
  • 柴油机启停成本;
  • 从主网购电的成本减去向主网售电的收益;
  • 储能充放电带来的电池退化成本。

用公式表达就是:

min Σ_{t=1}^{24} [ C_fuel(t) + C_su(t) + C_sd(t) + C_grid(t) + C_bat(t) ]

其中柴油机燃料成本可以做线性近似:

C_fuel(t) = a × P_dg(t) + b × u_dg(t)

这里P_dg(t)是柴油机出力,u_dg(t)是开机状态的0/1变量,a是边际燃料成本系数,b是空载固定成本。如果机组燃料曲线是凸的,这种线性近似已经足够;如果燃料曲线是非线性的,就需要引入分段线性函数做逼近。

主网交互成本可以写成:

C_grid(t) = p_buy(t) × P_buy(t) - p_sell(t) × P_sell(t)

其中p_buy和p_sell分别是购电和售电电价,P_buy和P_sell分别是购电功率和售电功率。正常情况下售电价会低于购电价,目标函数会自动避免同一时段既购电又售电,但如果你用的数据里出现倒挂,最好还是加一个购售互斥约束。

储能退化成本我建议一定要加。这个成本项本质上是给电池充放电行为一个“惩罚”,不然求解器会把储能当成免费工具,结果出现SOC曲线反复大起大落。虽然账面成本低了,实际电池寿命损失却被忽略了。

2.2 约束条件:一个都不能少

约束条件我列成一张速查表:

约束类型数学表达物理含义
功率平衡PV + VVT + DG + 放电 + 购电 = 负荷 + 充电 + 售电每个时段发用电必须瞬时平衡
柴油机出力上下限u_dg × P_min ≤ P_dg ≤ u_dg × P_max开机时出力在技术范围内
柴油机爬坡约束-RD ≤ P_dg(t) - P_dg(t-1) ≤ RU相邻时段出力变化受限
储能充放电上限0 ≤ P_ch ≤ u_ch × P_ch_max,0 ≤ P_dis ≤ u_dis × P_dis_max充放电功率不超过额定值
充放电互斥u_ch + u_dis ≤ 1不能同时充电和放电
SOC递推SOC(t) = SOC(t-1) + η_ch×P_ch×Δt/Cap - P_dis×Δt/(η_dis×Cap)荷电状态动态变化
SOC边界0 ≤ SOC(t) ≤ 1不超容量
购售电上限0 ≤ P_buy ≤ P_buy_max,0 ≤ P_sell ≤ P_sell_max受变压器容量和合同限制

这里特别想提醒功率平衡约束的符号。光伏和风电在等式里通常作为“已知量”移到等式右边,相当于负荷的一部分被新能源抵消了。储能充电是消耗功率的,所以要放在负荷侧;储能放电是提供功率的,要放在电源侧。很多人第一次建模时容易把储能充电功率符号写反,导致模型要么无解,要么出现能量凭空产生。

SOC递推约束是储能模型的核心。SOC(t)表示t时段结束时的荷电状态,SOC(t-1)是上一时段结束时状态,初始值SOC0需要给定。很多项目还会要求调度周期结束时SOC回到初始值,这可以作为一个等式约束加上,但注意这个约束会显著缩小可行域,如果实际场景允许,最好留一个松弛空间。

2.3 案例参数设定

为了让后面的代码有参照,我把案例设成一个小型微电网,所有功率单位统一用MW,时间单位统一用小时(h):

  • 柴油发电机:最小出力0.1 MW,最大出力1.5 MW,爬坡速率0.6 MW/h;
  • 储能系统:容量2 MWh,最大充电功率0.5 MW,最大放电功率0.5 MW,充放电效率均取0.95,初始SOC为0.5;
  • 与主网交互:购电上限1.0 MW,售电上限0.5 MW;
  • 分时电价:谷时0.30元/kWh,峰时0.90元/kWh,平时0.50元/kWh,简化成4个时段区间。

24小时负荷和新能源预测数据在实际项目中应该来自历史数据和天气预报,这里我直接用一段MATLAB代码生成示意序列,重点不是数据本身,而是后续的模型约束矩阵怎么构建。

3. MATLAB 调用 CPLEX 的完整实操流程

3.1 环境准备:安装 CPLEX 并打通 MATLAB 接口

MATLAB 调用 CPLEX 的第一步是安装求解器和添加接口路径。CPLEX Community Edition 可以从 IBM 官网免费下载,安装时记得勾选 MATLAB 支持组件。安装完成后,在 MATLAB 里添加对应的路径,不同版本路径不一样,我这里以 CPLEX Studio 22.1 为例:

addpath('C:\Program Files\IBM\ILOG\CPLEX_Studio2210\cplex\matlab\x64_win64'); savepath;

路径最后一般是x64_win64或x64_linux,具体看你操作系统和安装目录。添加完路径后,验证接口是否正常:

try cplex = Cplex(); disp('CPLEX接口OK'); catch ME disp(ME.message); end

如果这一步报错,最常见原因是路径没加对、MATLAB位数不是64位、或者安装目录里有中文和空格。我不建议把CPLEX装到带中文的路径下,mex编译文件对这种路径很敏感。

另外提醒一句:Community Edition对模型规模有限制,约束数、变量数和非零元总数都会有限制,用来做学术演示和小型工程案例没问题,但如果你的微电网节点很多、时段很长,建议申请 Academic Initiative 版本或者购买正式授权。

3.2 用原生 API 搭建 MILP 模型

我习惯先把所有决策变量按照“分块索引”的方式组织起来。假设全天分为N=24个时段,变量向量x的结构如下:

  • x(1:N):柴油机出力;
  • x(N+1:2N):柴油机开机状态,0/1变量;
  • x(2N+1:3N):储能充电功率;
  • x(3N+1:4N):储能放电功率;
  • x(4N+1:5N):充电状态,0/1变量;
  • x(5N+1:6N):放电状态,0/1变量;
  • x(6N+1:7N):购电功率;
  • x(7N+1:8N):售电功率;
  • x(8N+1:9N):SOC状态。

先定义参数和索引:

N = 24; dt = 1; % 小时 % 将下面换成实际预测数据 P_load = 0.4 + 0.2*sin((0:N-1)*pi/12) + 0.05*randn(1,N); P_pv = max(0, 0.5*sin((0:N-1)*pi/12)) .* ((0:N-1) > 6) .* ((0:N-1) < 18); P_wt = 0.1 + 0.05*cos((0:N-1)*pi/24); % 柴油机参数 dg_min = 0.1; dg_max = 1.5; dg_ru = 0.6; dg_rd = 0.6; fuel_a = 120; % 元/MWh fuel_b = 35; % 元/h % 储能参数 ess_cap = 2.0; ess_ch_max = 0.5; ess_dis_max = 0.5; eta_ch = 0.95; eta_dis = 0.95; soc0 = 0.5; % 主网价格,分4段 p_buy = [0.3*ones(8,1); 0.9*ones(6,1); 0.5*ones(4,1); 0.8*ones(6,1)]; p_sell = p_buy * 0.7; % 售电价 buy_max = 1.0; sell_max = 0.5; % 变量索引 idx.dg = 1:N; idx.u_dg = N+1:2*N; idx.ess_ch = 2*N+1:3*N; idx.ess_dis = 3*N+1:4*N; idx.u_ch = 4*N+1:5*N; idx.u_dis = 5*N+1:6*N; idx.buy = 6*N+1:7*N; idx.sell = 7*N+1:8*N; idx.soc = 8*N+1:9*N; nvars = 9*N;

这里用结构体idx存索引,后面拼约束矩阵时就能直接写idx.dg(3)这类语义清晰的代码,一旦模型规模扩大,这种编码方式也不容易乱。

目标函数向量和变量类型定义:

f = zeros(nvars, 1); f(idx.dg) = fuel_a; f(idx.u_dg) = fuel_b; f(idx.ess_ch) = 0.02; % 储能退化成本系数 f(idx.ess_dis)= 0.02; f(idx.buy) = p_buy; f(idx.sell) = -p_sell; lb = zeros(nvars, 1); ub = inf(nvars, 1); ub(idx.soc) = 1.0; ctype = repmat('C', 1, nvars); ctype(idx.u_dg) = 'B'; ctype(idx.u_ch) = 'B'; ctype(idx.u_dis) = 'B';

这里有一个小细节:售电收益在目标函数里是负成本,因为售电是收入,f(idx.sell)取负值即可。

接下来是约束矩阵构建。先写等式约束:功率平衡和SOC递推。

Aeq = sparse(2*N, nvars); beq = zeros(2*N, 1); % 功率平衡约束,前N行 for t = 1:N Aeq(t, idx.dg(t)) = 1; Aeq(t, idx.ess_dis(t)) = 1; Aeq(t, idx.buy(t)) = 1; Aeq(t, idx.ess_ch(t)) = -1; Aeq(t, idx.sell(t)) = -1; beq(t) = P_load(t) - P_pv(t) - P_wt(t); end % SOC递推约束,后N行 for t = 1:N r = N + t; Aeq(r, idx.soc(t)) = 1; if t > 1 Aeq(r, idx.soc(t-1)) = -1; end Aeq(r, idx.ess_ch(t)) = -eta_ch * dt / ess_cap; Aeq(r, idx.ess_dis(t)) = dt / (eta_dis * ess_cap); if t == 1 beq(r) = soc0; else beq(r) = 0; end end

不等式约束可以用一个大矩阵Aineq加一个列向量bineq来组装。以柴油机出力上下限和储能充放电上限为例:

Aineq = sparse(2*N, nvars); bineq = zeros(2*N, 1); % 柴油机出力上限:P_dg - P_max*u_dg <= 0 % 出力下限:P_min*u_dg - P_dg <= 0 row = 0; for t = 1:N row = row + 1; Aineq(row, idx.dg(t)) = 1; Aineq(row, idx.u_dg(t)) = -dg_max; bineq(row) = 0; row = row + 1; Aineq(row, idx.dg(t)) = -1; Aineq(row, idx.u_dg(t)) = dg_min; bineq(row) = 0; end

储能充放电上限、充放电互斥、购售电上限、爬坡约束同样往Aineq和bineq里追加即可。这里我就不再重复粘贴,核心思路是:每追加一类约束,就增加对应的行,最后统一传给CPLEX。

组装模型并求解:

Aall = [Aeq; Aineq]; lhs = [beq; -inf(size(Aineq,1), 1)]; rhs = [beq; bineq]; cplex = Cplex('microgrid_scheduling'); cplex.Model.sense = 'minimize'; cplex.Model.obj = f; cplex.Model.lb = lb; cplex.Model.ub = ub; cplex.Model.ctype = ctype; cplex.Model.A = Aall; cplex.Model.lhs = lhs; cplex.Model.rhs = rhs; cplex.write('microgrid_model.lp'); % 导出LP文件,便于调试 cplex.solve();

求解完之后,检查返回状态并提取结果:

if cplex.Solution.status == 101 || cplex.Solution.status == 102 x = cplex.Solution.x; P_dg_out = x(idx.dg); U_dg_out = x(idx.u_dg); P_ch_out = x(idx.ess_ch); P_dis_out = x(idx.ess_dis); SOC_out = x(idx.soc); P_buy_out = x(idx.buy); P_sell_out = x(idx.sell); else disp('求解失败,检查模型是否可行'); end

CPLEX里状态码101表示找到最优解,102表示在时间限制内找到可行解,通常也够用了。

3.3 结果分析与曲线绘制

结果出来之后,我通常会画两张图:第一张是功率平衡堆叠图,看每个时段的电源构成和负荷曲线;第二张是储能SOC曲线,看储能行为是否合理。

t_h = 0:N-1; figure; bar(t_h, [P_pv(:), P_wt(:), P_dg_out(:), P_dis_out(:), P_buy_out(:)], 'stacked'); hold on; plot(t_h, P_load(:), 'k-', 'LineWidth', 2); xlabel('时段/h'); ylabel('功率/MW'); legend('光伏','风电','柴油机','储能放电','购电','负荷'); figure; stairs(t_h, SOC_out, 'LineWidth', 1.5); xlabel('时段/h'); ylabel('SOC'); ylim([0 1]);

典型结果会出现几个规律:夜间光伏为零、负荷较低,储能在谷电时段充电;白天光伏大发、负荷上升,储能放电配合削峰;柴油机往往只在负荷高峰或者新能源出力不足的时段启动。如果电价峰谷差够大,储能的“低充高放”套利行为会非常明显,这也是微电网调度中最直观的经济性体现。

4. 常见问题与排查技巧实录

4.1 模型报 infeasible 怎么办?

模型无解是我遇到的第一个大坑。最常见的原因有三个:功率平衡约束符号写反、SOC初值终值条件互相矛盾、柴油机最小出力太大导致某些时段无论如何都平衡不了。

排查时我最推荐的办法是把LP文件导出,然后在CPLEX命令行工具或者用文本编辑器直接打开microgrid_model.lp,逐行检查等式约束:

c1: dg_1 + ess_dis_1 - ess_ch_1 + buy_1 - sell_1 = 0.1

如果发现常数项符号不对,就能在矩阵层面快速定位。还有一种方法是把等式约束先放宽成不等式,比如功率平衡允许小范围偏差,看模型是否可行,如果放宽后可行,说明等式内部逻辑有冲突。

4.2 求解时间太长怎么办?

当整数变量增多、约束规模上来之后,求解时间确实会拉长。我有三个常用手段:

第一,设置MIP gap和求解时间上限:

cplex.Param.timelimit.Cur = 300; cplex.Param.mip.tolerances.mipgap.Cur = 0.01;

允许1%的次优性,工程上通常是可以接受的,求解时间能下降一个量级。

第二,检查大M参数。储能充放电上限约束里,M值不需要取太大,直接取最大充放电功率0.5 MW就是最紧的取值。很多人习惯写一个很大的M=1000,这会让LP松弛质量变差,分支定界效率急剧下降。

第三,减少不必要的整数变量。比如把充电和放电状态变量去掉,改用“功率分段”的方式建模,虽然还是MILP,但变量结构更简洁,求解速度也会快一些。

4.3 MATLAB 和 CPLEX 版本对不上怎么办?

CPLEX每个版本对MATLAB的官方支持范围不完全一样,新MATLAB版本刚出来时,旧CPLEX的mex文件可能加载失败。解决办法有三个:升级CPLEX、降级MATLAB版本、或者检查路径里是不是同时存在多个CPLEX版本。

如果报错信息类似Unable to resolve the name cplex.Cplex,绝大多数情况是路径没有正确添加。运行一次ver('cplex')或者which Cplex.m就能看出来到底有没有加载成功。如果安装目录有中文或空格,建议卸载重装一次,路径问题真的不值得花几个小时去折腾。

4.4 结果曲线出现抖动和不合理跳变?

模型求解成功不代表结果合理。我见过最典型的“假最优解”是储能SOC曲线呈现出高频锯齿形状,每15分钟一个充电/放电切换,目标成本上显示很漂亮,实际运行根本执行不了。

这个问题主要是储能退化成本系数设得太小,求解器觉得频繁操作电池无成本,所以不心疼。解决方法是调大退化成本系数,或者加上最小充放电持续时间约束。

另一个常见问题是购电和售电同时为正。只要购电价高于售电价,目标函数会自动避免;但如果你的数据是平段和谷段价格倒挂,就需要加一个0/1互斥约束:

u_grid_buy + u_grid_sell ≤ 1

保证同一时段不会既买又卖。

5. 模型扩展方向与个人沉淀的经验

5.1 从确定性模型走向不确定优化

前面这套模型假设光伏、风电、负荷预测都是准确的,实际工程中预测误差不可避免。更进阶的做法是引入两阶段随机优化或鲁棒优化。MATLAB的优势在于可以方便地生成多个场景,把每个场景对应的一组功率平衡约束同时塞进模型,CPLEX依然能够求解。虽然模型规模和求解时间会上升,但对真实微电网运行来说,这种不确定性建模的价值非常直接。

5.2 与强化学习结合做在线调度

现在MATLAB里实现DQN、PPO等强化学习算法已经很常见。很多人问我,是不是学了强化学习就可以替代CPLEX做调度。我的看法是:确定性日前调度问题,用MILP和CPLEX求解,速度快、有全局最优保证,根本没有必要用强化学习硬碰;强化学习更适合在线决策、模型非线性强或者状态空间部分可观测的场景。如果你想做对比实验,可以先用这个MILP结果作为基准,再训练一个DQN或者PPO智能体,看在线调度策略和离线最优解之间差多少,这个方向很有意思。

5.3 几个值得长期保留的操作习惯

做完这个项目后,我给自己定了三条规矩,分享给你:

第一,每次求解前都导出LP文件,哪怕是已经优化好的模型也导出一次,一秒钟的事,关键时刻能救命。第二,功率单位全部统一成MW、时间单位统一成h,公式里到处是dt和效率,单位混了再好的模型也算不对。第三,每个场景的参数和结果都存成MAT文件,加load就能复现,后续改模型、调参数、做对比都方便。

这三个习惯看起来不起眼,但帮我省下了大量复查和返工时间。模型做完之后,我还会专门检查一次SOC曲线是否平滑、柴油机启停次数是否合理,这两个指标比单纯看总成本更能反映模型质量。微电网调度的奇妙之处就在于,把复杂的物理系统翻译成一组线性约束,再用好的求解器把它解开,而MATLAB与CPLEX正是这套翻译链路上非常顺手的一对搭档。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询