news 2026/10/19 5:38:43

基于YALMIP的微电网日前经济调度MILP建模与求解实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于YALMIP的微电网日前经济调度MILP建模与求解实践

做微电网优化调度这几年,我打交道最多的工具就是MATLAB里的YALMIP工具箱。它把“建模”和“求解”彻底分开,用接近数学公式的语法描述目标函数和约束,再交给底层求解器去算。这套基于YALMIP的微电网优化调度模型,核心是完成日前经济调度:给定未来24小时的负荷、光伏/风电预测出力和分时电价,求解蓄电池各时段充放电功率、以及从电网购电/售电的功率,在满足功率平衡、蓄电池SOC、购售电功率上限等约束的前提下,让总运行成本最低。如果你刚开始接触能量管理系统、微电网优化控制,或者正在做相关方向的课题、项目预研,这篇内容可以帮你把模型设计、代码实现和调试经验一次捋清楚。

1. 为什么选YALMIP:从调度问题本质到工具箱选型

1.1 微电网调度在数学上是什么问题

微电网的经济调度,本质上是一个带约束的最优化问题:在负荷、新能源出力、电价等边界条件给定的情况下,决定各可控设备在每个时段的出力,使得系统运行成本最低,同时满足设备物理限制和系统运行安全约束。

如果把蓄电池的充放电状态、购电和售电状态都用连续变量表示,且不做额外处理,模型会允许“一边充电一边放电”“一边买电一边卖电”这种荒谬结果。为了避免这种情况,必须引入0-1整数变量来描述状态切换。于是调度问题就从纯线性规划变成了混合整数线性规划,也就是常说的MILP。

MILP是求解微电网日前调度的标准数学框架。它的好处是:只要模型和参数正确,求解器就能给出全局最优解或带最优性间隙的近似最优解,不需要像启发式算法那样担心收敛到局部最优。对于n个时段的日前调度,变量规模通常只有几百个,现代求解器在几秒内就能完成求解,很快。

1.2 YALMIP解决的是什么痛点

很多人在没有接触YALMIP之前,习惯自己手写约束矩阵。比如用矩阵Aeq、beq表达等式约束,用A、b表达不等式约束,再把所有决策变量排成一列。这种方式的缺陷很明显:模型一复杂,变量编号就容易错;想加一个约束,所有矩阵维度都要跟着改;排查问题时根本不知道哪一行对应哪条物理约束。

YALMIP的价值在于,它让建模语言和数学模型保持一致。你可以直接用sdpvar定义连续变量,用binvar定义0-1变量,用==、>=、<=直接书写约束,用sum、乘积和循环组织目标函数。YALMIP会在后台把你写的表达式自动翻译成求解器需要的标准形式,通过optimize函数一次性完成求解。

我在实际项目中之所以一直推荐YALMIP,还有一个原因是它支持多种求解器,切换求解器只需要修改一行sdpsettings配置,模型本体不用动。这在做算法对比、验证不同求解器性能时非常方便。

1.3 求解器怎么选,license问题怎么处理

YALMIP本身不是求解器,它只负责建模,真正算MILP的是底层求解器。常见的商业MILP求解器性能很强,但license有门槛;开源求解器也能算出正确结果,只是大规模问题可能慢一些。对于本文这种单个微电网、24个时段的小规模问题,其实任何主流的MILP求解器都绰绰有余。

在YALMIP里配置求解器非常简单:

ops = sdpsettings('solver', '你的求解器名称', 'verbose', 2);

如果当前机器上没有可用的求解器,YALMIP会自动调用MATLAB自带的整数线性规划函数作为兜底。这里有一个经验:工程上不需要迷信求解器,反而应该把精力放在模型本身。模型错了,换再好的求解器也白搭;模型写对了,开源求解器一样能给出正确结果。

2. 模型核心构件:蓄电池、市场购售电与功率平衡

2.1 蓄电池SOC动态与充放电互斥

蓄电池是微电网调度中最核心的灵活资源。它的本质是一个带时间耦合的储能设备:上一时段的剩余电量会直接影响下一时段。这个动态过程用SOC递推式描述。

设调度时段数为n,每个时段长度为dt(小时),蓄电池额定容量为E_rating(kWh),初始SOC为SOC_init。定义Pch(t)为充电功率,Pdis(t)为放电功率,单位都是kW,且均非负。那么时段t末的SOC满足:

SOC(t+1) = SOC(t) + (η_ch * Pch(t) - Pdis(t) / η_dis) * dt / E_rating

其中η_ch是充电效率,η_dis是放电效率。如果取η_ch=0.95,意味着输入1kWh电能,最终存进去0.95kWh;放电时η_dis=0.9,意味着电池要放出1kWh电量,实际上需要从内部消耗1/0.9≈1.11kWh储能。这个单位换算最容易出错,后面调试部分会专门讲。

充放电互斥是必须处理的问题。如果不加约束,求解器可能会让蓄电池同一时刻既充电又放电,因为两笔能量在功率平衡中一正一负相互抵消,表面上不违反平衡,实际却损耗了大量能量,白浪费效率。所以需要两个0-1变量u_ch(t)和u_dis(t),分别表示充电状态和放电状态,再加上三组约束:

Pch(t) <= Pch_max * u_ch(t) Pdis(t) <= Pdis_max * u_dis(t) u_ch(t) + u_dis(t) <= 1

第三组约束确保同一时段最多只有一个状态为1。这种做法比直接限制Pch(t) * Pdis(t) == 0要靠谱得多,因为非线性约束会破坏MILP结构。逻辑和线性化的思路是:用大M法把“互斥”表达成线性不等式。

SOC本身还要满足上下限约束:

SOC_min <= SOC(t) <= SOC_max

很多实际项目要求调度周期结束时的SOC恢复到初始值附近,以便第二天继续调度。这会写成等式约束SOC(n+1) == SOC_init,或者允许在一个小区间内波动。

2.2 市场购电售电约束:状态互斥与功率上限

微电网与外部电网的功率交换,通常表现为两个方向:从电网购电和向电网售电。在没有两部制电价、没有需求响应的情况下,最简单的市场交互模型就是分时电价下的购售电决策。

定义Pbuy(t)为购电功率,Psell(t)为售电功率,单位kW,均非负。为了避免同一时段既购电又售电的无意义套利,引入购电状态u_buy(t)和售电状态u_sell(t),同样加互斥约束:

Pbuy(t) <= Pbuy_max * u_buy(t) Psell(t) <= Psell_max * u_sell(t) u_buy(t) + u_sell(t) <= 1

为什么必须互斥?因为如果购电价和售电价不一样,求解器可能会通过“高价售电、低价购电”来套利,实际系统中这往往不被允许,或者没有物理意义。即使购电价和售电价相等,同时购售电也会造成变压器和线路上的无谓流动。所以从模型保守性和工程合理性出发,互斥是必要的。

购售电功率上限一般来自变压器容量、并网线路容量或市场协议。如果微电网内部负荷很大,功率平衡会把购电量限制在合理范围内,但上限约束仍然不可少。

2.3 功率平衡约束:单母线模型的守恒方程

微电网调度常用的拓扑简化模型是单母线模型:忽略网损和电压分布,认为所有功率都汇聚在一条母线上,系统必须实时满足功率守恒。

功率平衡约束写成:

P_load(t) + Pch(t) == P_pv(t) + P_wind(t) + Pdis(t) + Pbuy(t) - Psell(t)

左边是负荷和充电功率,相当于“需求侧”;右边是光伏、风电、蓄电池放电和外部电网净输入,相当于“供给侧”。这只是一个等式约束,但由于它把多个变量耦合在一起,实际是模型中最关键的等式。

需要特别注意的是,有些初学者会把蓄电池充电放在等式右边,写成P_load == P_pv + P_wind + Pdis + Pbuy - Psell - Pch。两种写法数学等价,但物理含义完全不同。我建议统一采用“需求项在左、供给项在右”的写法,后期排查功率平衡时一眼就能看出哪个方向出了问题。

光伏和风电在日前调度中通常作为固定预测序列输入,不作为决策变量,因为对不可控电源的“调度”实际是通过弃光、弃风实现的,如果允许弃电,需要额外引入弃电变量和弃电惩罚。本文模型先不做弃电,假设预测出力全消纳。

3. 代码实现与细节解析

3.1 数据准备:把负荷、新能源和电价整理成向量

所有的优化调度代码,第一步都是数据准备。建议把数据整理成行向量或列向量,保持维度一致,后续直接参与矩阵运算。

n = 24; % 调度时段数:24小时 dt = 1; % 时段长度:1小时 P_load = [80, 75, 70, 68, 72, 85, 110, 135, 150, 145, 138, 130, ... 125, 128, 135, 142, 150, 155, 145, 130, 120, 105, 90, 82]'; % 单位kW P_pv = [0, 0, 0, 0, 0, 5, 20, 40, 65, 80, 90, 85, 75, 60, 45, 30, 15, 5, 0, 0, 0, 0, 0, 0]'; P_wind = [35, 40, 38, 30, 28, 25, 22, 20, 25, 30, 32, 35, 38, 40, 35, 30, 25, 20, 22, 28, 30, 32, 35, 38]'; % 分时电价,单位元/kWh buy_price = [0.35, 0.35, 0.35, 0.35, 0.35, 0.5, 0.8, 0.8, 0.8, 1.0, 1.0, 0.8, ... 0.8, 0.8, 0.8, 0.8, 1.0, 1.0, 1.0, 0.8, 0.5, 0.5, 0.35, 0.35]'; sell_price = buy_price * 0.8; % 售电价通常低于购电价

这是典型的数据格式。实际项目中这些数据可能来自预测系统或SCADA历史库,但进入优化器之前一定要确认单位统一。kW、kWh、元/kWh是三个基本单位,任何一处混用MWh、MW都会导致结果看起来合理,实际差1000倍。

3.2 定义YALMIP决策变量

接下来用YALMIP定义连续变量和二进制变量。变量命名建议和模型符号一一对应,方便后期检查。

Pdis = sdpvar(n, 1); % 蓄电池放电功率,kW,非负 Pch = sdpvar(n, 1); % 蓄电池充电功率,kW,非负 Pbuy = sdpvar(n, 1); % 购电功率,kW,非负 Psell = sdpvar(n, 1); % 售电功率,kW,非负 SOC = sdpvar(n + 1, 1); % SOC向量,下标1对应初始时刻,2对应时段1结束,依此类推 u_ch = binvar(n, 1); % 充电状态 u_dis = binvar(n, 1); % 放电状态 u_buy = binvar(n, 1); % 购电状态 u_sell = binvar(n, 1); % 售电状态

这里的SOC长度是n+1,因为我们需要n个时段对应的n个节点状态,再加上初始SOC。如果你习惯用n长度的SOC,也可以把递推式写成SOC(1)由初始值决定,但务必注意索引偏移。我自己的代码习惯是定义SOC(1)为时段1开始前,SOC(t+1)为时段t结束后,这样递推式写起来最直观。

3.3 约束搭建:蓄电池、购售电、功率平衡

约束部分按逻辑分块写,每一块用注释标明物理含义。

Constraints = []; % 蓄电池SOC动态与边界 SOC_init = 0.5; Soc_min = 0.1; Soc_max = 0.9; E_rating = 400; % 蓄电池额定容量,kWh eta_ch = 0.95; eta_dis = 0.9; Constraints = [Constraints; SOC(1) == SOC_init]; for t = 1:n Constraints = [Constraints; SOC(t + 1) == SOC(t) + ... (eta_ch * Pch(t) - Pdis(t) / eta_dis) * dt / E_rating]; end Constraints = [Constraints; Soc_min <= SOC(2:n+1) <= Soc_max]; % 可选:调度周期结束SOC恢复到初始值 Constraints = [Constraints; SOC(n + 1) == SOC_init]; % 充放电互斥和功率上限 Pch_max = 100; Pdis_max = 100; Constraints = [Constraints; Pch >= 0; Pdis >= 0]; Constraints = [Constraints; Pch <= Pch_max * u_ch]; Constraints = [Constraints; Pdis <= Pdis_max * u_dis]; Constraints = [Constraints; u_ch + u_dis <= 1]; % 购售电互斥和功率上限 Pbuy_max = 300; Psell_max = 200; Constraints = [Constraints; Pbuy >= 0; Psell >= 0]; Constraints = [Constraints; Pbuy <= Pbuy_max * u_buy]; Constraints = [Constraints; Psell <= Psell_max * u_sell]; Constraints = [Constraints; u_buy + u_sell <= 1]; % 功率平衡约束 Constraints = [Constraints; P_load + Pch == P_pv + P_wind + Pdis + Pbuy - Psell];

很多人在写SOC递推时忘记乘dt,或者忘记除以额定容量。这里帮你把物理量理一遍:功率(kW)乘以时间(h)得到能量(kWh),再除以额定容量(kWh)得到SOC增量。如果你用小时级调度,dt=1,乘不乘在数值上没差别,但换成15分钟调度、5分钟调度,dt等于0.25、0.083,漏乘就直接导致SOC不守恒。

3.4 目标函数与求解配置

目标函数是最小化运行成本。运行成本主要来自购电支出和售电收益,蓄电池本身没有燃料成本,但频繁充放电会衰减寿命,所以通常会加一个很小的循环损耗惩罚项,让求解器不会出现无意义的来回充放电。

% 购电成本 - 售电收益 Cost = sum(Pbuy .* buy_price - Psell .* sell_price); % 蓄电池循环损耗惩罚(可选,量纲为元/kWh) alpha = 0.001; Cost = Cost + alpha * sum(Pch + Pdis) * dt; % 求解 ops = sdpsettings('solver', '', 'verbose', 2, 'showprogress', 1); result = optimize(Constraints, Cost, ops);

solver留空时,YALMIP会自动选择合适的已安装求解器。如果想要更详细的求解日志,可以把solver指定为你机器上实际可用的求解器。如果结果显示有警告,可以用yalmiperror(result.problem)查看错误含义。

3.5 结果提取与基础绘图

求解完成后,用value函数提取变量数值。因为YALMIP变量是符号对象,不能直接当数值用。

Pdis_opt = value(Pdis); Pch_opt = value(Pch); Pbuy_opt = value(Pbuy); Psell_opt = value(Psell); SOC_opt = value(SOC); t = 1:n; figure; subplot(2,1,1); bar(t, [P_load, P_pv + P_wind, Pdis_opt, Pch_opt, Pbuy_opt, Psell_opt], 'stacked'); legend('负荷', '新能源', '放电', '充电', '购电', '售电'); xlabel('时段/h'); ylabel('功率/kW'); subplot(2,1,2); stairs(t, SOC_opt(2:n+1), 'LineWidth', 1.5); xlabel('时段/h'); ylabel('SOC'); ylim([0 1]);

绘图不是为了好看,而是验证结果的第一道关卡。我最常看的图就是功率堆叠图和SOC曲线。只要功率平衡正确,堆叠图上负荷与各电源的差值应该始终为0;SOC应该被限制在上下限之内,且如果想看有没有振荡,SOC曲线应该平滑。

4. 调试实录:常见报错与排查技巧

4.1 求解不可行:从哪入手

solve之后如果result.problem == 1,说明模型不可行。千万不要急着删约束,而是用“逐步放宽”的思路定位问题。

我通常按以下顺序排查:

  • 先检查功率平衡等式是否可能成立。把所有变量上限带入等式左右两侧,看有没有交集。比如负荷是150kW,光伏+风电+放电+购电上限之和如果小于负荷+充电下限,那系统无论如何都平衡不了。
  • 再检查SOC的初始值和终点值。如果初始SOC是0.5,但允许区间是[0.1,0.9],终点又强制回到0.5,在新能源极少、负荷极大的时段可能没有足够能量给电池充电。
  • 然后检查互斥约束是否“过紧”。互斥逻辑一般不会导致不可行,但如果你同时限定了某个时段只能购电不能售电,而负荷又很小,新能源又很大,多余的功率没法消纳,就会不可行。此时要增加售电上限或允许弃电。
  • 最后用check(Constraints)命令查看哪条约束的残差非零,这是个很好的定位手段。

4.2 蓄电池SOC越界或充放电同时发生

SOC越界的常见原因有三个。第一是单位不一致,把MWh当kWh用,导致SOC增量被放大1000倍;第二是没加SOC上下限约束;第三是递推式漏了效率,导致能量不守恒出现漂移。

如果你发现结果里同一个时段u_ch和u_dis同时为1,那不是YALMIP写错了,很可能是没加u_ch + u_dis <= 1。也有一种情况:即使加了互斥,由于数值容差,求解器可能输出0.9998和0.9997这种接近1的小数,看起来像同时为1。这时可以用round或设定阈值做后处理,把浮点结果归一到0/1。

4.3 求解速度慢:调优方向

本文这个规模的MILP,正常应该秒解。如果发现求解很慢,优先检查是不是不小心引入了非线性。比如写了Pch(t) * u_ch(t)、SOC(t) * Pdis(t)这类乘积,模型就从MILP变成MIQP或MINLP,求解难度会大幅上升。

YALMIP对约束表达式有诊断机制,求解前会提示“Nonconvex”之类的信息。保持模型线性是速度快的前提。其次,可以通过设置求解器MIP gap参数来加速:

ops = sdpsettings('solver', '你的求解器', 'solver.options.mipgap', 0.01);

MIP gap设为1%通常足够了,工程精度完全够用。

4.4 常见问题速查表

下面这个表是我实际调试中总结的,可以帮你快速定位问题。

现象可能原因排查思路
求解结果SOC一直是0.5没写SOC递推约束,或递推式索引错误打印SOC_opt,检查每个时段是否有变化
购电和售电同时大于0忘记加互斥约束,或者互斥约束没包含在Constraints中检查u_buy/u_sell约束
功率不平衡,误差恒等于某个常数负荷和新能源方向定义反了用手算验证某个典型时段
结果出现很大的正负峰值数据里有坏点、缺失值,或电价单位错误画原始曲线检查数据
求解耗时几十秒模型非线性,或整数变量太多检查有没有变量乘积;可尝试减少二进制变量
使用某个求解器报错求解器license不可用或YALMIP配置名不对先solver=''让YALMIP自动选,或换开源求解器

5. 从单日模型到实际工程扩展

5.1 把日前调度改成滚动优化

实际微电网运行中,预测数据会随时间更新。与其每天早上算一次24小时计划,不如每15分钟或每小时滚动重算一次,把当前实际SOC作为新的初始值,只执行未来一段时间的指令。这就是滚动优化,也叫模型预测控制。

把前述模型改成滚动只需要做三处调整:一是把n改成滚动时域长度,比如16个时段;二是把SOC初始值改为当前实测值;三是根据需要在目标函数里加入终端惩罚项,防止最后SOC被“耗尽”。滚动优化的效果一般来说比单次日前调度更稳,因为它能不断修正预测误差。

5.2 考虑预测不确定性:从确定性到鲁棒

光伏和风电预测一定有误差。如果你希望结果在预测偏差下仍可行,可以把不确定性建模成区间,使用鲁棒优化。YALMIP对鲁棒优化有专门的uncertain支持,可以把不确定参数声明出来,让YALMIP自动转化为鲁棒对等模型。

另一种更工程化的做法是场景法:生成多个可能的预测场景,让调度结果在所有场景下都满足约束,目标函数取期望成本。场景数量不宜太多,否则模型会变得很大。经验上,10到20个场景对日前调度已经能显著提升鲁棒性。

5.3 继续加约束的路线图

微电网优化的拓展方向很多。比如:

  • 加入柴油机/燃气轮机:增加出力变量、启停状态、启动成本和爬坡约束,模型仍保持MILP。
  • 加入备用约束:要求系统在最大机组停运或负荷突变时仍有足够旋转备用。
  • 加入变压器容量约束:对应非线性交流潮流,如果只是单向潮流约束,可以保持线性。
  • 加入需求响应:把可转移负荷建模为带时序耦合的决策变量,类似蓄电池的时间平移特性。
  • 加入多微电网互联:让多个微电网之间可以相互买卖电力,每对节点之间需要额外的交易变量和交易状态。

这些扩展的核心思路都是一样的:先把物理过程总结成变量和约束,再把非线性逻辑通过二进制变量线性化,最后交给YALMIP统一求解。所以你会模型这一段打牢了,后面加东西都很快。

代码写多了之后,最大的体会是:模型能跑出数只是第一步,结果合理才是关键。每次求解完,我都会把SOC曲线、购售电计划、功率平衡残差三条曲线打出来看一眼。平衡残差不是零,说明某个变量方向定义反了;SOC波动异常,多半是容量或效率写错。YALMIP这个工具能帮你省掉大量矩阵书写的麻烦,但不能帮你省掉对物理过程的思考。建议拿到代码后,先试着把负荷、光伏、风电数据换成你自己的,再把约束一条条注释掉看结果变化,慢慢就能体会到每个约束在模型里扮演什么角色。这个从“跑通代码”到“看透模型”的过程,才是做优化调度真正值钱的地方。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 5:36:49

OpenCV张正友标定:工业相机内参求解与去畸变实战

简介&#xff1a;这是一套基于OpenCV实现张正友相机标定的完整工程代码&#xff0c;主要面向计算机视觉初学者、机器人导航与三维重建领域的开发者&#xff0c;解决相机内参与外参求解、畸变矫正等基础但关键的问题。工程基于VS2015编写&#xff0c;包含详细注释与一键运行脚本…

作者头像 李华
网站建设 2026/10/19 5:36:42

分块Attention性能暴跌70倍?长序列推理显存与速度优化实战

1. 从一次压测翻车说起&#xff1a;分块 attention 的显存与速度悖论那天晚上&#xff0c;我在一台单卡 24GB 显存的机器上跑一个 7B 模型的推理压测。序列长度拉到 8192&#xff0c;batch size 开到 16&#xff0c;用默认的 attention 后端跑&#xff0c;显存直接爆了。这在意…

作者头像 李华
网站建设 2026/10/19 5:35:56

开源免费!12合1洋芋田图像工具箱:本地图片处理替代Photoshop

做图像处理久了&#xff0c;总会遇到一类非常尴尬的场景&#xff1a;手里只有一张截图&#xff0c;想压缩到50KB发到工作群&#xff1b;拍了三张手机截图&#xff0c;想拼成一张完整长图&#xff1b;或者手头有几十张照片&#xff0c;需要批量加水印、改尺寸、转格式。这些事说…

作者头像 李华
网站建设 2026/10/19 5:35:39

aepsych实战:贝叶斯优化驱动的主动采样阈值估计

这两年我一直在跟心理物理学实验打交道&#xff0c;最常被周围人问的问题不是“刺激怎么呈现”&#xff0c;而是“阈值到底怎么估才又快又稳”。传统 staircase 虽然直接&#xff0c;但步长设置、反转判定、初期大范围试错&#xff0c;每一步都在消耗被试的耐心和实验时间。后来…

作者头像 李华
网站建设 2026/10/19 5:34:29

Playwright codegen与AI补全:UI自动化脚本的工程化实践

运行了近一年的 UI 自动化项目后&#xff0c;我愈发觉得“录制脚本”这件事被很多人低估了。尤其当 AI 编程助手越来越普及的时候&#xff0c;一说到生成自动化脚本&#xff0c;大家下意识就想让 AI“从零写一段”。但我在自己的多个项目里&#xff0c;几乎都是反着来的&#x…

作者头像 李华
网站建设 2026/10/19 5:33:42

Kubernetes容器资源限制实战:requests、limits与QoS详解

从接触Kubernetes到能在生产环境里睡个安稳觉&#xff0c;中间隔着的不是一堆概念&#xff0c;而是对“资源限制”这套机制真正的理解。我见过太多团队&#xff0c;刚开始用k8s时把requests和limits当成两个可填可不填的字段&#xff0c;结果服务一上线就频繁重启&#xff0c;节…

作者头像 李华