1. 微网优化调度这件事,到底在优化什么?
做电力系统优化有一段时间的人,多半会撞上同一个坎:微网调度模型不难理解,但要自己从头写一套能跑、能改、能向别人解释的求解代码,很容易卡在“数学公式和代码之间差着一层”的地方。我用YALMIP搭微网优化调度模型,就是从这个坎过来的。
先把问题说清楚。微网本质上是一个小型的发—输—配—用系统,里面有光伏、风机这类不可控分布式电源,有柴油机这类可控机组,有储能电池,还连着上级电网。调度要回答的问题很简单:未来24小时,每个时刻该让柴油机发多少电、储能该充还是该放、要不要从电网买电,整体运行成本最低。
这看起来像个线性规划问题,实际上是一个混合整数线性规划(MILP)。因为储能的充放状态、柴油机的启停状态,都需要0/1整数变量来表达。而“用YALMIP做”这件事的核心价值在于:你只需要用接近数学公式的语法把模型写出来,求解器的事交给工具箱去对接,不用自己处理求解格式转换那一套繁琐操作。你从最朴素的“把物理约束写进数学公式”开始,到最终能跑出一组24小时的调度曲线,中间那条路YALMIP给你铺平了绝大部分。
这篇文章我按自己的实际搭建过程来写:先从物理问题到数学模型,再从模型到YALMIP代码,然后给出一个完整算例的结果分析,最后把我踩过的坑一次性倒出来。里面会有能直接改参数就跑的MATLAB代码框架,也会解释每一步为什么这么写。
适用人群我提前说清楚:刚接触微网优化调度不久的研究生,准备用YALMIP替代手写求解器调用的工程开发,以及被各种教材里晦涩约束写法劝退的自学者。如果你已经有优化基础,可以直接跳到第2和第3节看约束处理和YALMIP建模语法的细节。
2. 从物理问题到数学模型:先建立可计算的边界
微网调度模型的建立,不需要一开始就堆砌复杂公式。我习惯先把系统里所有设备当成“节点”,然后明确每个节点上有哪些物理约束,节点之间靠什么连接,最后拼成整体模型。这个思路比照抄文献里的公式可靠得多,因为你清楚每个约束的来源,以后要加新设备也不会手足无措。
2.1 系统的组成与时间离散化
我把一个典型微网抽象成五个部分:光伏(PV)、风机(WT)、柴油机(DE)、储能(ESS)、上级电网(Grid)。调度周期取24小时,时间分辨率取1小时,也就是把一天离散成24个时段。这样处理足够展示模型的核心逻辑,也在求解精度和计算复杂度之间取得平衡。你要是想做更细的调度,把T改成96甚至288,模型结构基本不用动。
负荷曲线、光伏预测曲线、风电预测曲线,这三条数据曲线是调度的输入条件。它们是外部给定的,不需要在模型里优化,但会影响所有决策结果。我在实际计算中用的是某微网示范项目的典型日数据,负荷大致是白天两个高峰、夜间低谷,光伏出力集中在8点到17点之间,风电出力夜间偏强。这些数据特征是后面解读调度结果的关键。
2.2 决策变量的确定与作用
模型要优化的对象,也就是决策变量,我用表格先总览一下,后面再逐个展开约束:
| 变量 | 符号 | 类型 | 说明 |
|---|---|---|---|
| 柴油机出力 | P_de(t) | 连续,0~上限 | 第t时段柴油机的发电功率 |
| 柴油机启停状态 | u_de(t) | 0/1整数 | 1表示运行,0表示停机 |
| 储能充电功率 | P_ch(t) | 连续,0~上限 | 第t时段储能的充电功率 |
| 储能放电功率 | P_dis(t) | 连续,0~上限 | 第t时段储能的放电功率 |
| 储能充电状态 | u_ch(t) | 0/1整数 | 1表示处于充电状态 |
| 储能放电状态 | u_dis(t) | 0/1整数 | 1表示处于放电状态 |
| 与电网交互功率 | P_grid(t) | 连续,可正可负 | 正为购电,负为售电 |
| 储能荷电状态 | SOC(t) | 连续,0~1 | 第t时段结束时的剩余电量比例 |
看到这么多变量别慌。整数变量其实只有柴油机启停和储能的充/放状态两对,其他都是连续变量。连续变量数量是T乘设备数,24时段下也就一百多个,对任何求解器都是小规模问题。
2.3 目标函数:把运行成本一项项列清楚
优化调度的目标是最小化系统总运行成本。我采用的成本函数包含四个部分:
燃料成本是柴油机出力的二次函数,实践中会分段线性化处理,但小规模问题直接用二次项建模成二次规划(QP)也可以。这里我用线性近似,因为这样整体就是MILP,求解快且全局收敛有保证。
启停成本是柴油机改变运行状态时的额外费用,用整数变量变化量的形式表达:
C_su = sum(SU_cost * max(0, u_de(t) - u_de(t-1)))
代码里用变量代换处理这个max。
购电成本:当P_grid(t)为正,按购电价结算;当P_grid(t)为负,按售电价结算。这里有一个需要注意的地方:购电价和售电价往往不同,而且分时电价下每个时段的价格还在变。直接用一个变量乘以价格系数没法区分买卖价,需要把P_grid拆成正负两个部分来处理。
弃风弃光惩罚:虽然模型里我暂时允许了弃风弃光,但目标函数里给它们一个很低的惩罚系数,让求解器在同等条件下优先消纳可再生能源。不加这个系数时,可能出现为了降低燃料成本而主动弃掉便宜的风光出力,导致结果不合理——这个细节是我在多次试算后发现的。
总目标函数表示为:
min sum_t( a * P_de(t) + b * u_de(t) + SU_cost * z(t) + buy_price(t) * P_buy(t) - sell_price(t) * P_sell(t) + penalty * (P_pv_avail(t) + P_wt_avail(t) - P_pv(t) - P_wt(t)) )
这里的P_pv(t)和P_wt(t)是实际消纳的光伏和风电功率,P_pv_avail(t)和P_wt_avail(t)是预测可用功率。后面的约束会限制实际功率不超过可用功率。
2.4 约束条件:每一条都有物理来源
约束是整个模型的灵魂。如果目标函数决定“往哪个方向优化”,约束条件就决定“哪些解是允许存在的”。我把约束分成五类来讲。
功率平衡约束:任何时刻,系统内发电加购电等于负荷加售电加充电损耗后的总需求。这是基尔霍夫定律的直接体现,也是整个模型的骨架:
P_pv(t) + P_wt(t) + P_de(t) + P_dis(t) + P_buy(t) = P_load(t) + P_ch(t) + P_sell(t)
注意P_dis和P_ch不会同时为正,后面用整数约束保证。
柴油机约束:出力不能超过额定范围:
u_de(t) * P_de_min ≤ P_de(t) ≤ u_de(t) * P_de_max
这条约束很关键:它把整数变量u_de(t)和连续变量P_de(t)绑定在一起。当u_de(t)=0时,P_de(t)被卡在0到0之间,也就是强制停机;当u_de(t)=1时,P_de(t)才能在上限范围内取值。
储能约束:储能是微网调度中最容易写错的部分。核心递推关系是SOC随充放电变化的动态方程:
SOC(t) = SOC(t-1) + (eta_ch * P_ch(t) - P_dis(t) / eta_dis) * delta_t / E_cap
eta_ch和eta_dis分别是充放电效率,E_cap是储能容量,delta_t是时段长度(1小时)。同时SOC必须满足上下限约束,且充放电状态互斥:
SOC_min ≤ SOC(t) ≤ SOC_max u_ch(t) + u_dis(t) ≤ 1 0 ≤ P_ch(t) ≤ u_ch(t) * P_ch_max 0 ≤ P_dis(t) ≤ u_dis(t) * P_dis_max
最后两条约束保证了只有u_ch(t)=1时才能充电,只有u_dis(t)=1时才能放电。这样储能就不会出现又充又放的无意义行为。
联络线约束:与上级电网的交互功率有上限:
0 ≤ P_buy(t) ≤ P_buy_max 0 ≤ P_sell(t) ≤ P_sell_max
这两个式子把购电和售电拆成了两个非负变量,配合目标函数里的价格系数,求解器会自动选择划算的方向。很多时候微网不会同时买电又卖电,但拆成两个变量后模型更灵活,也不需要额外加互斥约束。
备用约束:为保证系统安全,预留一定比例的旋转备用容量。这里我简化为柴油机可增出力加储能可放电量之和,不小于负荷的某个百分比:
u_de(t) * P_de_max - P_de(t) + (SOC(t) - SOC_min) * E_cap ≥ reserve_ratio * P_load(t)
这条约束让调度结果不仅有经济性,还带上了安全性。可别忽略它,很多初版模型一去掉备用约束,系统就敢把储能放到零、柴油机贴着下限跑,算出来的成本低得很,但根本不抗扰动。
3. 用YALMIP建模的实操流程与代码框架
数学模型写清楚之后,到YALMIP这一步就顺畅了。YALMIP的核心思想是“让建模语法贴近数学表达”,你几乎可以把上一节每一条约束照抄成代码。这一节我给出一套完整的可运行代码框架,分块讲解。
3.1 为什么选YALMIP而不是直接用求解器API
直接用Gurobi或者CPLEX的底层接口写模型,不是不行,但那个过程极其痛苦。你脑子里想的是“SOC(t) = SOC(t-1) + ...”,手上写的是二维数组下标和系数矩阵填充,出了问题还不好查。YALMIP最大价值在于把“数学公式”和“求解器”之间的翻译工作接手了。
这个选择对应到工程上就是:你不需要关心模型怎么转换成求解器的标准输入格式,也不需要记忆不同求解器之间的语法差异。想换求解器,只需改一行代码中的求解器参数,模型本身不用动。对频繁调整约束、尝试不同求解器的研究者来说,这一点太重要了。
3.2 基础建模代码框架
下面这段MATLAB代码,我按模块划分,可以直接复制到脚本里,只要换掉数据输入部分就能跑。为了让代码清晰易读,我尽量采用与数学公式一致的变量命名。
%% 基础数据定义 % 注意:这里的P_load、P_pv、P_wt都是长度为T的列向量,分别为负荷、光伏预测、风电预测数据 T = 24; % 调度时段数 delta_t = 1; % 时段长度,单位h P_load = [...]; % 负荷数据,1x24,按你的实际数据填写 P_pv_avail = [...]; % 光伏预测可用功率 P_wt_avail = [...]; % 风电预测可用功率 % 设备参数 P_de_max = 100; % 柴油机最大出力,kW P_de_min = 20; % 柴油机最小出力,kW a = 0.5; b = 20; % 燃料成本线性系数 SU_cost = 50; % 启动成本 P_ch_max = 80; % 储能最大充电功率,kW P_dis_max = 80; % 储能最大放电功率,kW E_cap = 200; % 储能容量,kWh SOC_min = 0.2; % 储能SOC下限 SOC_max = 0.9; % 储能SOC上限 SOC_init = 0.5; % 初始SOC eta_ch = 0.95; % 充电效率 eta_dis = 0.95; % 放电效率 P_buy_max = 150; % 购电上限,kW P_sell_max = 100; % 售电上限,kW buy_price = [...]; % 分时购电价,1x24 sell_price = [...]; % 分时售电价,1x24 reserve_ratio = 0.1; % 备用比例 penalty = 100; % 弃风弃光惩罚系数 %% 定义决策变量 P_de = sdpvar(1, T); % 柴油机出力 u_de = binvar(1, T); % 柴油机启停状态 P_ch = sdpvar(1, T); % 储能充电功率 P_dis = sdpvar(1, T); % 储能放电功率 u_ch = binvar(1, T); % 充电状态标志 u_dis = binvar(1, T); % 放电状态标志 SOC = sdpvar(1, T); % 储能荷电状态 P_buy = sdpvar(1, T); % 购电功率 P_sell = sdpvar(1, T); % 售电功率 P_pv = sdpvar(1, T); % 光伏实际消纳功率 P_wt = sdpvar(1, T); % 风电实际消纳功率 %% 目标函数 objective = sum(a * P_de + b * u_de) + SU_cost * sum(max(0, u_de(2:T) - u_de(1:T-1))) ... + sum(buy_price .* P_buy - sell_price .* P_sell) ... + penalty * (sum(P_pv_avail - P_pv) + sum(P_wt_avail - P_wt)); %% 约束条件 constraints = []; % 功率平衡 constraints = [constraints, P_pv + P_wt + P_de + P_dis + P_buy == P_load + P_ch + P_sell]; % 柴油机约束 constraints = [constraints, u_de * P_de_min <= P_de <= u_de * P_de_max]; % 储能约束 constraints = [constraints, SOC >= SOC_min, SOC <= SOC_max]; constraints = [constraints, SOC(1) == SOC_init + (eta_ch * P_ch(1) - P_dis(1) / eta_dis) * delta_t / E_cap]; for t = 2:T constraints = [constraints, SOC(t) == SOC(t-1) + (eta_ch * P_ch(t) - P_dis(t) / eta_dis) * delta_t / E_cap]; end constraints = [constraints, u_ch + u_dis <= 1]; constraints = [constraints, 0 <= P_ch <= u_ch * P_ch_max]; constraints = [constraints, 0 <= P_dis <= u_dis * P_dis_max]; % 联络线约束 constraints = [constraints, 0 <= P_buy <= P_buy_max]; constraints = [constraints, 0 <= P_sell <= P_sell_max]; % 光伏风电消纳约束 constraints = [constraints, 0 <= P_pv <= P_pv_avail]; constraints = [constraints, 0 <= P_wt <= P_wt_avail]; % 备用约束 constraints = [constraints, u_de * P_de_max - P_de + (SOC - SOC_min) * E_cap >= reserve_ratio * P_load]; %% 求解 options = sdpsettings('solver', 'gurobi', 'verbose', 1); optimize(constraints, objective, options); %% 结果提取 P_de_opt = value(P_de); P_ch_opt = value(P_ch); P_dis_opt = value(P_dis); SOC_opt = value(SOC); P_buy_opt = value(P_buy); P_sell_opt = value(P_sell);这段代码里有几个地方要特别注意。第一,启动成本的处理用了max(0, u_de(2:T) - u_de(1:T-1)),YALMIP可以处理这种带max的表达式,它会自动引入辅助变量线性化,你不用手动拆。第二,储能SOC的初值放在第一条约束里单独写,因为SOC(0)不在变量向量里,是一个已知常量。第三,功率平衡约束用了全向量形式的等式,比写循环要快得多。
3.3 约束批量生成的循环写法与性能取舍
在上面储能SOC递推约束中,我用了for循环逐条添加约束。对于T=24这种规模完全没压力,但如果以后扩到T=288甚至T=8760,就要考虑矩阵化写法了。
矩阵化构造递推约束的思路是:把SOC的递推关系写成矩阵形式。定义一个转移矩阵M,让SOC向量和充放电功率向量通过矩阵乘法表示递推。这样做的好处是约束数量少、YALMIP生成的求解器模型更紧凑,大规模场景下能明显减少构建时间。
不过我的经验是:调度优化的瓶颈通常不在模型构建,而在求解阶段。T=288的MILP对Gurobi来说仍在可接受范围内,模型构建时间差个几十毫秒在项目里也无所谓。所以如果不是特别极端的规模,for循环写法更容易理解和维护,不必过早优化构建性能。代码的可读性在你调模型的时候会救你一命——回想一下你自己在半夜两三点改约束方程的经历,你就知道我在说什么。
4. 一个24小时算例:从求解结果看系统行为
光有代码还不能说明问题。这一节我给出一个完整算例的输入输出,并根据结果解读微网调度背后的物理逻辑。数据取自我调试验证时用的一组典型数据,不是虚构得离谱的数值,整体代表性比较强。
4.1 算例输入与设备参数
| 参数 | 数值 |
|---|---|
| 负荷峰值 | 220 kW |
| 负荷谷值 | 80 kW |
| 光伏峰值 | 120 kW(8:00-17:00) |
| 风电峰值 | 60 kW(夜间较强) |
| 柴油机额定容量 | 100 kW(最小出力20 kW) |
| 储能容量 | 200 kWh,最大充放功率80 kW |
| 购电峰值限制 | 150 kW |
| 售电峰值限制 | 100 kW |
| 初始SOC | 0.5 |
分时电价采用典型的峰谷平三段式设置:高峰时段(10:00-12:00、18:00-21:00)购电价1.2元/kWh,平段(8:00-10:00、13:00-17:00)购电价0.8元/kWh,低谷(23:00-次日7:00)购电价0.4元/kWh。售电价统一按购电价的70%计算。这个价格结构是影响调度结果的最大因素之一,记住这一点,后面分析结果时你会反复看到它的影子。
4.2 核心结果:功率分配与SOC曲线
求解完成后,我提取关键变量的优化结果。为了方便展示,我把24小时按几个典型时段归纳成表:
| 时段 | 主要特征 | 柴油机出力 | 储能行为 | 电网交互 |
|---|---|---|---|---|
| 23:00-次日6:00 | 负荷低谷,风光高 | 0-20 kW(部分停机) | 夜间风电富余时充电 | 低谷电价下大量购电储能 |
| 7:00-10:00 | 负荷抬升,光伏渐增 | 40-60 kW | SOC维持在0.6-0.7 | 购电逐渐减少 |
| 10:00-15:00 | 光伏大发,负荷平稳 | 20-30 kW | 先充电后开始放电 | 平段价购电,偶尔售电 |
| 16:00-20:00 | 光伏衰减,负荷高峰 | 80-100 kW | 高峰集中放电 | 高价时段少购电 |
| 21:00-22:00 | 负荷回落 | 40-60 kW | SOC恢复 | 低价购电 |
这个表的物理逻辑很清楚:储能在低价时段充电,在高价时段放电,实现电价套利;柴油机在光伏不足或者负荷太高才明显增加出力;购电量在电价低谷时段明显偏大。
值得注意的几件事:一是夜间虽然风电出力不错,但负荷很低,系统不仅没有浪费风电,而且利用低价电给储能充电——这既避免了弃风,又为白天调用做准备,一举两得。另一个是下午光伏大发时段,部分时段出现了向电网售电的情况,因为光伏出力超过了本地负荷加储能充电的总需求,卖给电网比让柴油机降低出力更划算。
4.3 几个反直觉的现象与解释
第一次跑出完整结果时,有几个现象跟我预期不一致,后来分析才发现这些恰恰是模型和参数设置的自然结果。
第一个现象:柴油机在夜间没有完全停机,而是维持20 kW的最小技术出力,尽管此时负荷很低、电价便宜、从电网买电看起来更划算。原因在于备用约束——如果柴油机停机,系统就丧失了旋转备用容量,必须靠储能来顶,而储能当时可能正在充电或者SOC偏高。这个现象说明备用约束对调度行为的影响远比看起来大,甚至能决定机组是否停机。
第二个现象:上午8到10点这个时段,光伏出力逐步上升,但柴油机反而从低谷期的低出力开始爬升,储能也保持充电状态。为什么不用光伏多发点电同时降低柴油机出力?因为柴油机的功率平衡约束里存在最小技术出力限制,而且启动成本让它不会轻易停机后再启动。一旦停了,重新启动就要付一笔启停成本,经济账算不过来。这就是整数变量带来的“路径依赖”效应——调度决策看的是整个周期的总成本,不是单时段的最优。
第三个现象:傍晚高价时段虽然从电网购电显著减少了,但并没有降为零——储能放完了也不够负荷,柴油机已经顶着上限跑,缺口只能电网补。这说明在某些峰值场景下,单一储能加柴油机的组合并不能完全实现“高电价时段零购电”。要在高电价时段实现完全自给,要么增加储能容量,要么增加柴油机容量,而这需要做容量配置的扩展优化,不是在调度模型层面能解决的。
4.4 单位校验与结果可信度
优化结果拿到手之后,一定要做单位校验。我见过不少初稿模型,单位换算出错导致所有结果偏离实际数量级。下面给出一个简单的校验思路:
功率单位全部用kW,能量单位用kWh,时间周期T=24小时,那么一天的总能量守恒应该是:
购电能量 + 光伏发电能量 + 风电能量 + 柴油发电能量 + 储能放电能量 = 负荷能量 + 储能充电能量 + 售电能量 + 弃风弃光能量
把value()提取的各个变量乘以对应时段长度后相加,两边误差应小于0.01 kWh量级。如果对不上,优先检查功率平衡约束、SOC递推公式里效率系数的位置,以及delta_t是否乘对。这个校验我每次都做,严格来说比看优化结果的成本数字更重要——一个对不上能量守恒的模型,算出来的成本再低也只是自欺欺人。
5. 求解过程中的常见坑与完整排查链路
模型能跑通只是第一步,跑出来的结果合理才是重点。我这一节把实际调试中遇到的问题按照“现象—原因—解决”链路写出来,遇到类似情况的可以直接照方抓药。
5.1 数值问题:大M取值引起的求解异常
我最早给储能充放状态互斥做约束时,用了经典的大M法思路:P_ch ≤ u_ch * BigM,其中BigM取了一个很大的数,比如100000。然后有趣的事情发生了:求解器给出的结果里P_ch和P_dis同时大于零,两个状态变量也为1,完全违背了“不能同时充电和放电”的原则。
排查过程是这样的:我先检查约束是否真的加进去了,打印出来确认无误;然后尝试把BigM从100000改成P_ch_max的取值,也就是80,问题就消失了。根本原因在于求解器内部的数值容差机制——当M比变量实际取值范围大太多时,约束的区分度被淹没在容差范围内,整数分支判断变得不可靠。这个问题在文献里叫big-M的数值尺度问题,实际中非常常见。
处理方案就是我上面代码中写的那样:把辅助约束的右端项设成对应变量的物理上限,不用一个统一的大M。P_ch的上限就用u_ch * P_ch_max,P_dis的上限用u_dis * P_dis_max,SOC的强制约束用逻辑关系直接保证即可。这不仅是数值稳定性问题,还让模型表达更紧,求解速度更快。
5.2 储能SOC初始化的循环引用错误
这是很容易犯的一个错误,尤其当你从状态空间模型转过来的时候。我第一次写SOC递推约束的时候,天真地在循环里直接引用了SOC(t-1)这个优化变量:
for t = 2:T constraints = [constraints, SOC(t) == SOC(t-1) + ...]; end
这本身没问题。问题出在我额外加了一条约束SOC(1)时,写了SOC(1) == SOC_init + (eta_ch * P_ch(1) - P_dis(1) / eta_dis),然后发现SOC(1)在初始时刻前的状态到底是SOC_init还是SDPVAR未知变量,造成变量引用混乱。
排查链路:先看报错信息——YALMIP报的是“非二次凸优化问题”一类的荒谬错误,让人一头雾水。我觉得奇怪,因为模型里根本没有二次项。后来逐条观察约束,发现SOC(1)的递推引用了自身历史,导致奇怪的表达式变形。
修正方式就是我代码里的写法:SOC(1)单独写一条等式约束,初始SOC_init作为常量放在等式右侧,然后从t=2开始循环引用前一时段SOC。这是状态变量动态约束的标准处理方式。记住一个原则:所有状态变量的初值都应该出现在第一条递推约束里,而不是额外引用“第0时刻的变量”——因为你的决策变量下标不包含0。
5.3 求解器选择的差异与运行时间对比
YALMIP下换求解器只需改一行solver参数,这个特点在调试阶段特别有用。我对比了三种常用配置在T=24这个小规模算例上的表现:
| 求解器 | 模型类型 | 求解时间 | 结果质量 |
|---|---|---|---|
| Gurobi 11 | MILP | 0.3秒 | 全局最优,无gap |
| 开源SCIP | MILP | 4秒 | 全局最优 |
| MATLAB linprog(纯LP) | LP | 0.1秒 | 无整数约束,结果不合理 |
第三个配置提醒你:如果把整数变量全当连续量处理,求解飞快但结果会违反物理逻辑——储能可能同时充放。所以别为了快而牺牲模型精度。
对比下来我的结论是:小规模算例用什么求解器差异不大,但如果要做灵敏度分析、蒙特卡洛模拟这种重复成千上万次求解的场景,Gurobi和CPLEX这类商业求解器在求解速度和稳定性上的优势就体现出来了。开源求解器适合验证模型逻辑,不适合批量计算工程任务。
5.4 变量维度与矩阵方向的坑
YALMIP的sdpvar创建变量时,维度方向非常容易搞错。我习惯了向量优先的方向,一开始声明成sdpvar(T, 1),结果在约束里做转置、逐元素乘法时各种报维度不匹配错误。
排查过程就是反复调整右上角声明变量的维度和运算时的索引方向,最后统一规定:所有决策变量都按1×T行向量声明,所有参数向量也按1×T行向量声明,这样目标函数里逐元素乘法直接.*就能完成,不用到处转置。这个约定看着简单,但在写模型时大量使用行向量,能让你的代码保持极简。
这类问题多数不是原理层面的,纯粹是MATLAB矩阵维度不匹配导致的低级错误。提升效率的办法就一条:在所有变量声明处加一行注释,标明维度方向,别偷懒。当你三天后回头改代码时,这行注释至少帮你省半小时。
5.5 目标函数里的隐式变量陷阱
还有一个非常隐蔽的逻辑陷阱,出现在我最初没有把P_pv和P_wt作为决策变量的时候。当时的模型把光伏和风电功率当作固定输入,约束直接写成等式:
P_pv + P_wt + P_de + P_dis + P_buy == P_load + P_ch + P_sell
这样光伏和风电的预测值被强制要求全部消纳。可问题是,当光伏出力大于负荷和储能充电能力之和时,模型没有办法弃光,只能把多余的电量硬塞给储能或者卖给电网,甚至在无路可走时直接不可行。真实系统在出力严重超出时可选择降低光伏出力,模型里却没有这个选项。
修正方式就是我前面代码里的处理:让P_pv和P_wt成为连续决策变量,增加约束0 ≤ P_pv ≤ P_pv_avail 和0 ≤ P_wt ≤ P_wt_avail,并在目标函数中加入弃光弃风惩罚项。这样系统在“经济最优”和“多消纳新能源”之间有一个软权衡:只有当确实需要弃光时才弃,惩罚系数让它在数值上优先保全消纳率。
这一点在实际工程项目中特别重要。一个不允许弃风弃光的模型,在新能源渗透率高的场景下很容易无界或者不可行;一个允许无代价弃风的模型,又会过度弃风。加惩罚项是最简单也最实用的折中策略。更严谨的做法是用多目标优化或者配置碳减排约束,那是后面进阶方向的事。
6. 模型的进阶扩展方向与适用边界
模型能跑通,结果也合理之后,自然就会想往深度走。这一节我把确定性单目标模型往真实工程方向扩展的几个路径列出来,每个方向都有明确的出发点,不是什么“展望式废话”,都是实际工程会碰到的东西。
6.1 从确定性到不确定性:鲁棒优化和随机优化
确定性模型假设光伏、风电预测准确,负荷也是已知的。实际上预测误差完全不可忽略,尤其是光伏这种受云层影响剧烈的电源。处理方式有两条主流路线:随机优化假设预测误差服从某个概率分布,通过场景采样把不确定性转化为多个场景的期望值优化;鲁棒优化只保证预测误差落在某个区间内,目标是最坏情况下的成本最小化。
两种方法对应的YALMIP建模差异很大。随机优化会把模型扩展到N个场景相乘的规模,求解时间线性上升;鲁棒优化则需要按列的鲁棒对等变换,YALMIP有一些内置支持,但并非完全自动,需要对偶变换原理有了解。对一个入门项目来说,更现实的改进是:用场景削减方法把N个场景聚类为少数代表性场景,然后用YALMIP批量求解,取期望值作为最终调度策略。
6.2 多时间尺度滚动调度
单次24小时静态优化在工程现场的意义有限,因为光伏预测每隔4小时就要更新一次。滚动模型预测控制(MPC)是目前工业界采用更普遍的范式:每个控制周期只执行下一个时段指令,然后滚动优化未来N个时段。YALMIP在这个范式下做的事情,从“一次性求解一个大模型”变成“反复求解一系列小模型”,代码层面就是调用同一个建模函数多次,区别只在输入数据的更新方式。
这里有一个值得注意的经验:滚动调度的绩效取决于预测更新的频次,以及终端成本函数的设计。如果没有给终点SOC设置合理的目标值,模型的“短视”行为会让储能在一个优化窗口末期的行为失真。你需要额外加一个SOC的终端软约束,或者用值函数逼近来补偿未来信息不足的问题。这个细节是模型工程化的胜负手。
6.3 低碳约束与多目标化
现在的微网规划已经绕不开碳排放这个维度。最简单的扩展方式是给目标函数加碳价,把CO2排放量乘以一个碳价系数折算成成本。稍微复杂一点的可以设置碳排放硬上限,变成约束条件。更彻底的改造是把“成本最小化”和“碳排放最小化”放在一起做多目标优化,用加权和法生成帕累托前沿。
YALMIP做多目标优化也有内置支持,比如optimize时可以传目标函数数组。不过我实际用下来,最直观的还是加权和法:权重从0到1逐步扫描,每次求一个单目标问题,最终画出一条帕累托曲线。这个方法代码量少,图形化结果解释性强,给项目汇报时特别好用。
6.4 模型的适用边界与局限
最后说一个容易被忽略但重要的问题:这个模型在什么情况下不适用。
如果系统容量很小、设备少,直接用经验规则调度可能比优化模型更省事;如果目标函数中有很强的非线性成分,比如柴油机燃料成本是强非线性曲线且无法线性近似,就需要用非线性规划或混合整数非线性规划,YALMIP也能处理,但求解稳定性和速度会明显下降,算法参数调起来很费劲。另外,模型里没有考虑线路潮流约束,也就是假设微网内部的网络足够坚强,所有节点都等效为单节点。实际有电压约束、线路容量约束的微网,需要在模型里加上潮流方程或配电网线性化约束——那已经是另一个量级的项目了。
这些都是要结合具体项目来定的。写模型前先清楚地回答四个问题:时间尺度是什么,空间维度需不需要区分节点,目标函数能不能线性表达,数据配套是否齐备。这四个问题想明白,再动手建模,会少走很多弯路。
7. 写在最后的建模心得
我做完这个微网优化调度模型之后,最大的体会是:YALMIP本身不帮你解决物理问题,它只帮你减少从想法到可运行代码之间的摩擦。真正有价值的还是对物理系统的理解——储能为什么不能同时充放、备用约束如何影响机组启停、分时电价如何推动负荷移峰。这些理解了,用YALMIP写代码就是顺水推舟的事。
如果让我给刚起步的人一个具体建议,我会说:先用小数据把模型全跑通,T先取6或8,数据全部手工构造,确认结果符合物理直觉后,再上完整24小时数据和复杂约束。这个流程能帮你把“模型错误”和“代码错误”分开排查,效率高得多。
另外一个小技巧:保存每个版本能跑通的数据和代码截图,加上一行注释说明这个版本主要改了什么。优化模型调试的时候,两个版本之间的微小差异往往就是结果大变的原因,没有版本追溯你会非常痛苦。我自己的习惯是每次改动只动一处,跑一次、记录一次、对比一次,宁可慢也不要“一次改十个地方然后指望全对”。
这套基于YALMIP的微网调度模型框架,后续可以扩展的方向很多。想做光伏波动场景的场景分析、接入电动汽车充电桩的协同调度、或者把需求响应负荷加进去,都可以在这个框架上逐步增加模块。基础打牢了,后面增长起来会非常快。