做电力系统调度优化的同学,应该都绕不开机组组合问题。机组组合(Unit Commitment,UC)说白了就是回答一个每天都在发生的问题:未来24小时,电网里哪几台机组该开着、哪几台该停掉、每台发多少电量,才能在满足负荷和运行约束的前提下,把总成本压到最低。这题目听起来像排个值班表,但真做起来才发现,背后全是混合整数规划、爬坡约束、启停时间限制、备用容量分配这些硬骨头。这篇笔记我结合自己用MATLAB做的一个小算例,把“考虑热备用和安全约束的机组组合优化调度”从模型搭建到代码实现完整走一遍。适合正在做电力系统优化课程设计、毕设,或者刚入调度岗想搞清楚SCUC(Security-Constrained Unit Commitment,安全约束机组组合)原理的朋友。
我最初接触这个题目的时侯,以为只要把负荷平衡和机组出力上下限写上去就能跑,结果第一次求解出来的方案在凌晨时段频繁启停机组,调度员要是真拿着这种计划去操作,电厂那边估计得骂人。后来才明白,机组组合不是简单的经济调度,它最大的难点在于把电力系统运行中的各种物理规律和可靠性要求翻译成数学约束。特别是热备用约束,它直接决定了系统在失去一台最大机组或负荷突然上涨时能不能扛得住;安全约束则决定了发出来的电能不能通过输电网送到用户端。这两类约束加进来之后,问题复杂度立刻上了一个台阶,但也真正贴近了工程实际。
1. 先把问题说透:机组组合到底在干什么
1.1 火电机组的“热惯性”决定了计划不能拍脑袋
电力系统有一个非常特殊的性质:电能难以大规模存储,发多少就要用多少,系统时刻处于动态平衡之中。用户侧的空调、电机、照明负荷每时每刻都在变化,调度员必须提前安排好发电侧的“兵力部署”,确保负荷涨上来的时候有人能顶上。
火电机组不是按一下开关就能立刻满发的设备。一台煤电机组从冷态启动到并网带满负荷,往往需要几个小时甚至十几个小时,因为锅炉要加热、汽轮机要暖机、各种辅机要逐步投入。如果等到负荷突增再临时开机,黄花菜都凉了。所以调度部门必须提前一天甚至更早,基于负荷预测结果,确定次日各个时段的开机组合。这就是机组组合优化问题的来源:在已知未来负荷曲线的条件下,决定每台机组在哪些时段运行、哪些时段停机,以及运行时发多少电。
在这个决策过程中,成本是核心驱动因素。不同机组的经济性差异很大,大型高效机组单位发电成本低,小机组高;频繁启停又会带来额外的燃料消耗和设备损耗。机组组合优化的目标就是在成本和安全之间找平衡点:该开的机组不能少,能省的钱尽量省。
1.2 热备用是系统安全的底线,不是额外的发电量
热备用这个概念,很多刚接触电力系统优化的同学容易理解偏。热备用不是指“多留一些发电能力备用”,而是指那些已经并网运行、处于热状态、可以在短时间内增加出力的机组容量。与之对应的是冷备用,冷备用机组没有并网,从启动到并网需要很长时间。在系统遭遇机组跳闸、负荷预测偏差、新能源出力波动等突发情况时,能快速响应的只有热备用。
热备用约束的工程意义非常直观:系统中任何一台机组突然退出运行,剩下的在线机组必须有能力在短时间内把出力顶上去,否则系统频率就会跌落,严重时可能导致更大范围的连锁故障。因此调度规程通常要求系统的热备用容量不低于最大单机容量或系统最大负荷的一定比例。
但是热备用不是白给的。一台机组要提供热备用,就必须留出可上调的出力空间,这意味着它不能发到满负荷,把本可以低价发出来的电量“闲置”了。从经济角度看,热备用需求越大,系统被迫多开机组、降低高效机组出力的可能性就越大,运行成本也就越高。机组组合模型里,热备用约束就是把这种“安全底线”转化为数学不等式,逼着优化算法在成本和可靠性之间做一个折中。
1.3 从UC到SCUC:把输电网络的“水桶效应”放进来
早期教材里讲的机组组合,往往只考虑“机组—负荷”之间的功率平衡,也就是把所有发电机和所有负荷看作一个点,只要总发电量等于总负荷量就行。这种简化在电力系统发展初期、电网结构简单的时代是可以接受的。但现在的大规模互联电网,输电线路的输送能力往往成为限制发电计划落地的瓶颈。
举一个很典型的例子:某大型高效机组位于西部电源基地,低成本电量通过长距离输电线路送往东部负荷中心。如果机组组合计划只考虑总功率平衡,让这台机组满发,算下来成本很低,但实际执行时却发现送电线路容量不够,导致潮流越限。调度员拿到这种计划根本无法执行,只能手工调整。安全约束机组组合(SCUC)就是在传统机组组合模型的基础上,把输电线路的潮流约束、节点电压约束(在直流潮流模型下主要是线路功率约束)纳入优化模型中,确保组合方案在物理上可执行。
从数学模型的角度看,SCUC比传统UC多了网络约束方程,问题的约束规模显著增加,求解难度也水涨船高。但对于现代电力系统运行而言,这一步绕不开。中风、光伏等新能源大规模接入后,输电通道阻塞问题越来越频繁,不考虑网络约束的机组组合方案基本没有实用价值。
2. 数学模型:把调度经验翻译成约束条件
2.1 目标函数:用最少的钱,办可靠的事
机组组合优化的目标函数看起来简单——总运行成本最小——但拆开来其实有三部分:燃料成本、启动成本和停机成本。燃料成本是运行成本的大头,通常用机组出力P的二次函数近似:
[ C_{fuel}(P) = aP^2 + bP + c ]
其中a、b、c是机组的煤耗特性系数,不同机组差异很大。用二次函数描述的燃料成本曲线有一个特点:边际成本随出力增加而上升,这符合热力学的实际情况——机组接近额定出力时效率下降,单位发电成本上升。
启动成本是机组从停机状态转为运行状态时的一次性成本,包括锅炉点火、暖管、燃料消耗等费用。启动成本与停机时间长短有关,停机时间越长,机组冷却越充分,重新启动需要消耗的能量越多。在简化模型中,通常按固定值处理,或者分冷启动、温启动、热启动三档。停机成本相对较小,很多模型干脆把它并入启动成本或者直接忽略。
用数学语言描述,目标函数写成分段形式:
[ \min \sum_{t=1}^{T}\sum_{i=1}^{N} \left[F_i(P_{i,t}) + SU_i \cdot s_{i,t}\right] ]
其中t是时段序号,i是机组序号,(F_i(P))是机组i的燃料成本函数,(SU_i)是启动成本,(s_{i,t})是启动事件变量,表示机组i在时段t是否从停机转为运行。
在实际编程中,二次成本函数会给混合整数规划求解器增加负担。因为intlinprog这类求解器要求目标函数是线性的,处理非线性目标需要用分段线性化技巧。我在算例中先采用线性燃料成本,即斜率固定,即每个机组只有一个边际成本系数,这样模型简洁,新手容易读懂。如果需要更精确的煤耗曲线,可以在跑通基础模型之后再按分段线性化的方式扩展目标函数。
2.2 机组侧约束:一长串物理限制,一个都不能少
机组组合模型里,机组自身的约束是最容易遗漏、也最容易写错的。这些约束直接反映了机组的物理特性和运行规则,每一条背后都有具体的工程背景。
首先是出力上下限约束:
[ P_{i,min} \cdot u_{i,t} \le P_{i,t} \le P_{i,max} \cdot u_{i,t} ]
其中(u_{i,t})是机组i在时段t的运行状态,1表示运行,0表示停机。这个约束的逻辑是:机组停机时出力必须为0;开机时出力不能低于最小技术出力(低于这个值锅炉燃烧不稳定),也不能超过额定最大出力。注意(u)乘在两侧,才能让停机时段出力为零。
其次是爬坡约束。机组出力不能瞬间变化,受制于锅炉和汽轮机的热应力,机组在相邻两个时段之间的出力变化量有一个上限。数学上写为:
[ P_{i,t} - P_{i,t-1} \le RU_i + (2-u_{i,t}-u_{i,t-1}) \cdot M_i ]
[ P_{i,t-1} - P_{i,t} \le RD_i + (2-u_{i,t}-u_{i,t-1}) \cdot M_i ]
这里RU和RD分别是向上、向下爬坡速率,M是一个足够大的数(通常取机组的最大出力)。这个写法用大M法处理启停状态切换的时段:如果机组在t和t-1都处于开机状态,右边就是爬坡速率本身,约束生效;如果有一个时段机组是停机的,右边被放大到M量级,约束自动放宽。这个技巧非常实用,很多教材里直接写爬坡约束而没有大M项,在机组没有连续运行时会导致模型无解。
最小运行时间和最小停机时间约束,是机组组合模型中最能体现“状态记忆”的约束,也是最容易把求解器搞到崩溃的约束。机组一旦启动,不能运行一两个小时代就停,这会烧坏设备;同样,停机之后必须经过足够的冷却时间才能再次启动。这类约束在数学上有多种建模方法,我采用的是启动事件变量法:
定义(s_{i,t})表示机组i在时段t启动(从0变1),(d_{i,t})表示在时段t停机(从1变0)。最小运行时间约束可以写成:如果机组在t时刻启动,那么在后续(T_{up})个时段内都必须保持运行:
[ \sum_{k=t}^{t+T_{up}-1} u_{i,k} \ge T_{up} \cdot s_{i,t} ]
同理,如果机组在t时刻停机,那么在后续(T_{down})个时段内必须保持停机:
[ \sum_{k=t}^{t+T_{down}-1} (1 - u_{i,k}) \ge T_{down} \cdot d_{i,t} ]
这种约束在电网实际运行中的意义很好理解。我一开始做模型时,采用了一个极其简化的版本——只约束相邻两个时段的启停切换,结果求解出来的方案让一台大型机组一天启停了4次,现实中电厂绝对不可能配合这种操作,这让我意识到最小启停时间是必须严格建模的。
2.3 系统侧约束与直流潮流模型
机组侧的约束全部齐活之后,还需要把系统层面的约束加进来。最基本的系统约束是功率平衡约束:
[ \sum_{i=1}^{N} P_{i,t} = D_t ]
即每个时段所有机组的出力总和必须等于负荷需求。这个约束看起来简单,但要注意单位统一和网络损耗的问题。交流潮流模型里还有网损,需要额外处理;在直流潮流模型下,通常忽略网损,直接令发电等于负荷。
热备用约束是安全性的核心体现:
[ \sum_{i=1}^{N} R_{i,t} \ge R_t^{req} ]
其中(R_{i,t})是机组i在时段t可提供的热备用容量。关键问题在于(R_{i,t})怎么算。最准确的模型是:
[ R_{i,t} = \min(P_{i,max} - P_{i,t},\ RU_i) \cdot u_{i,t} ]
这个式子的物理含义是:一台机组能提供的备用,既要看它还有多少出力空间(Pmax减去当前出力),又要看它的爬坡能力能不能在短时间内把电量增发出来。停机状态的机组不能提供热备用,所以要乘以(u)。这个约束在MILP中需要引入辅助变量并线性化,我会在代码部分演示具体做法。
安全约束即网络约束。在SCUC中,最常用的简化模型是直流潮流模型。直流潮流假设线路电阻远小于电抗、电压接近额定值、相角差足够小,从而把交流潮流简化为线性方程组,可以方便地嵌入优化模型。支路潮流的计算公式为:
[ P_{ij,t} = b_{ij}(\theta_{i,t} - \theta_{j,t}) ]
其中(b_{ij})是线路电纳,(\theta)是节点相角。线路潮流必须满足容量限制:
[ -P_{ij,max} \le P_{ij,t} \le P_{ij,max} ]
加上网络约束之后,机组组合问题变成真正的SCUC问题。算出来的结果不仅告诉你哪些机组该开,还能告诉你发电计划不会让任何一条线路超载。
3. MATLAB代码实现:从公式到可运行的优化模型
3.1 建模工具怎么选:YALMIP + intlinprog还是直写intlinprog
用MATLAB做机组组合优化,建模方式主要有三条路:第一,手写intlinprog的矩阵参数,把约束逐条变成A、b矩阵;第二,用YALMIP这类建模工具箱,用类似数学公式的方式描述约束,再调用底层的求解器;第三,用MATPOWER等专用电力系统分析工具自带的功能。
我的建议是:只要你不是在生产环境中追求极致的运行效率,就用YALMIP。原因是机组组合模型的约束量很大,手动整理矩阵又容易出错,而且一旦模型需要调整——比如加一台机组、改一个备用比例——手写矩阵版的改动成本极高。YALMIP允许你直接用符号变量描述约束表达式,代码可读性和可维护性都高出好几个档次。
求解器方面,如果你没有额外安装Gurobi或CPLEX,用MATLAB自带的intlinprog就够了。intlinprog是MATLAB内置的混合整数线性规划求解器,对于几十台机组、24个时段的算例,求解速度完全可以接受。我自己的经验是,10台机组以内的中小算例,intlinprog通常在几秒到几十秒内就能收敛到最优解。
3.2 数据准备与决策变量定义
先定义一个小的测试系统。为了便于读者复现,我用一个6节点、4台火电机组的系统,负荷曲线也是我自己设计的典型日负荷形状。机组参数如下表所示:
| 机组 | 所在节点 | Pmax(MW) | Pmin(MW) | 爬坡(MW/h) | 最小运行时间(h) | 最小停机时间(h) | 启动成本(元) | 燃料成本系数(元/MWh) |
|---|---|---|---|---|---|---|---|---|
| G1 | 1 | 300 | 60 | 120 | 4 | 4 | 800 | 18 |
| G2 | 2 | 220 | 40 | 90 | 3 | 3 | 600 | 20 |
| G3 | 3 | 180 | 30 | 80 | 2 | 2 | 400 | 23 |
| G4 | 4 | 100 | 20 | 50 | 1 | 1 | 200 | 28 |
这四台机组的经济性差异比较明显,G1煤耗最低、容量最大,G4容量最小、成本最高。在负荷低谷时段,最优方案大概率只开G1或者G1+G2;只有在负荷很高或者备用需求压力大的时候,G3、G4才会被调用起来。
系统共有7条支路,拓扑设计成一个接近实际电网的环形结构。负荷主要分布在节点4、5、6,其中节点4还有一台小机组G4,这样能给“本地出力缓解阻塞”提供可能性。各支路参数如下:
| 支路 | 起点 | 终点 | 电纳(p.u.) | 容量(MW) |
|---|---|---|---|---|
| L1 | 1 | 2 | 0.25 | 200 |
| L2 | 1 | 4 | 0.28 | 180 |
| L3 | 2 | 3 | 0.22 | 160 |
| L4 | 2 | 5 | 0.30 | 180 |
| L5 | 3 | 6 | 0.26 | 150 |
| L6 | 4 | 5 | 0.24 | 150 |
| L7 | 5 | 6 | 0.27 | 160 |
64时段日负荷曲线,我按典型“峰谷平”三段式设置,凌晨低谷约300MW,白天高峰接近600MW。负荷分配到节点4、5、6的比例大概是40%、30%、30%。
在MATLAB代码里,数据部分长这样:
% 24时段负荷曲线(单位MW) D = [320,310,300,295,300,320,380,460,520,560,580,570,... 550,540,560,590,600,580,560,540,500,460,420,360]; % 机组数据矩阵 % [Pmax Pmin RU RD MinUp MinDown StartCost FuelCost] gen = [ 300, 60, 120, 120, 4, 4, 800, 18; 220, 40, 90, 90, 3, 3, 600, 20; 180, 30, 80, 80, 2, 2, 400, 23; 100, 20, 50, 50, 1, 1, 200, 28; ]; % 支路数据矩阵 % [from to b(p.u.) FlowMax(MW)] branch = [ 1, 2, 0.25, 200; 1, 4, 0.28, 180; 2, 3, 0.22, 160; 2, 5, 0.30, 180; 3, 6, 0.26, 150; 4, 5, 0.24, 150; 5, 6, 0.27, 160; ];这个负荷曲线的数据量级和实际电网相比缩小了不少,但保持了“凌晨低、白天高、晚高峰回落”的真实形态,用来展示机组组合的逻辑已经足够。
用YALMIP定义决策变量时,机组状态变量是24×4的0-1矩阵,出力变量是24×4的连续变量矩阵,另外还需要定义启动事件变量、停机事件变量和直流潮流中的相角变量:
T = 24; ng = 4; N = 6; L = size(branch,1); u = binvar(T, ng, 'full'); % 机组运行状态 P = sdpvar(T, ng, 'full'); % 机组出力 su = binvar(T, ng, 'full'); % 启动事件 sd = binvar(T, ng, 'full'); % 停机事件 r = sdpvar(T, ng, 'full'); % 机组热备用容量 theta = sdpvar(T, N, 'full'); % 节点相角 Pflow = sdpvar(T, L, 'full'); % 支路潮流3.3 核心约束代码与求解流程
在YALMIP中建立约束的代码,基本上就是把数学模型逐条翻译过来。首先是机组自身的约束:
Constraints = []; % 出力上下限 for t = 1:T for i = 1:ng Constraints = [Constraints, ... gen(i,2) * u(t,i) <= P(t,i) <= gen(i,1) * u(t,i)]; end end % 爬坡约束(包含启停放宽的大M项) for t = 2:T for i = 1:ng Constraints = [Constraints, ... P(t,i) - P(t-1,i) <= gen(i,3) + (2-u(t,i)-u(t-1,i)) * gen(i,1)]; Constraints = [Constraints, ... P(t-1,i) - P(t,i) <= gen(i,4) + (2-u(t,i)-u(t-1,i)) * gen(i,1)]; end end然后是启动事件和停机事件的定义,以及最小启停时间约束。这里的关键点是,启动事件变量必须由状态变量u的差分来定义,同时用不等式把它的取值“夹”在合理的范围内:
% 启动/停机事件定义 for t = 2:T for i = 1:ng Constraints = [Constraints, ... su(t,i) >= u(t,i) - u(t-1,i)]; Constraints = [Constraints, ... su(t,i) <= u(t,i)]; Constraints = [Constraints, ... su(t,i) <= 1 - u(t-1,i)]; Constraints = [Constraints, ... sd(t,i) >= u(t-1,i) - u(t,i)]; Constraints = [Constraints, ... sd(t,i) <= u(t-1,i)]; Constraints = [Constraints, ... sd(t,i) <= 1 - u(t,i)]; end end % 最小运行/停机时间 for i = 1:ng for t = 2:T % 最小运行时间 uptime = gen(i,5); Constraints = [Constraints, ... sum(u(t:min(T, t+uptime-1), i)) >= uptime * su(t,i)]; % 最小停机时间 downtime = gen(i,6); Constraints = [Constraints, ... sum(1-u(t:min(T, t+downtime-1), i)) >= downtime * sd(t,i)]; end end这里t从2开始,是因为初始时刻t=1没有前一个状态可以参照。对于机组在t=1时刻的初始状态,我在算例中假设所有机组在调度日开始时处于运行状态且已经满足了最小启停时间,这样避免处理边界条件的复杂性。实际调度的场景中,前一天的计划会给出一组初始状态,需要在模型中额外加约束来衔接。
接下来是系统约束和热备用约束。热备用约束的线性化是一个常见的考点:
% 系统功率平衡 for t = 1:T Constraints = [Constraints, sum(P(t,:)) == D(t)]; end % 热备用约束 R_req = 120; % 系统所需热备用容量,MW for t = 1:T for i = 1:ng Constraints = [Constraints, r(t,i) >= 0]; Constraints = [Constraints, r(t,i) <= gen(i,1) - P(t,i)]; Constraints = [Constraints, r(t,i) <= gen(i,3) * u(t,i)]; end Constraints = [Constraints, sum(r(t,:)) >= R_req]; end注意热备用约束里,r(t,i)同时受“剩余出力空间”和“爬坡能力”两条上限约束。第四条约束r <= RU*u是关键中的关键:如果机组处于停机状态,备用量强制为0,否则模型会在停机机组上分配出不存在的备用容量。
直流潮流约束和支路潮流约束如下:
% 构建节点电纳矩阵 B = zeros(N, N); for k = 1:L i = branch(k,1); j = branch(k,2); b = branch(k,3); B(i,i) = B(i,i) + b; B(j,j) = B(j,j) + b; B(i,j) = B(i,j) - b; B(j,i) = B(j,i) - b; end Bred = B(2:end, 2:end); % 直流潮流约束 genBus = [1, 2, 3, 4]; for t = 1:T Pinj = zeros(N,1); for i = 1:ng Pinj(genBus(i)) = Pinj(genBus(i)) + P(t,i); end Pinj = Pinj - [0; 0; 0; 0.4*D(t); 0.3*D(t); 0.3*D(t)]; Constraints = [Constraints, theta(t,1) == 0]; Constraints = [Constraints, Bred * theta(t,2:end)' == Pinj(2:end)]; end % 支路潮流计算与限制 for t = 1:T for k = 1:L i = branch(k,1); j = branch(k,2); b = branch(k,3); Constraints = [Constraints, ... Pflow(t,k) == b * (theta(t,i) - theta(t,j))]; Constraints = [Constraints, ... -branch(k,4) <= Pflow(t,k) <= branch(k,4)]; end end这个代码片段里有一个细节值得注意:负荷在节点4、5、6上的分配,用0.4、0.3、0.3的比例乘上总负荷。节点4有发电机G4,也有负荷,它的净注入是出力减去本地负荷。
最后是目标函数和求解命令:
% 目标函数:燃料成本 + 启动成本 FuelCost = sum(sum(gen(:,8) .* P)); StartCost = sum(sum(gen(:,7) .* su)); Cost = FuelCost + StartCost; % 设置求解器并求解 ops = sdpsettings('solver', 'intlinprog', 'verbose', 2); sol = optimize(Constraints, Cost, ops);在YALMIP中,求解完成之后可以用value()函数提取每个变量的数值。比如:
u_opt = value(u); P_opt = value(P); Pflow_opt = value(Pflow);我习惯先把结果存成表格和图形,再细看每一个时段的开机组合和每条线路的潮流情况。这样可以快速发现模型中的逻辑错误,比如某条线路潮流数据不符合基尔霍夫定律,说明约束漏了或者方向写反了。
4. 算例结果:热备用和安全约束如何改变组合方案
4.1 测试系统配置总览
在展示结果之前,先回顾一下算例的整体配置。这是一个6节点系统,节点1、2、3、4分别接入一台机组,节点4同时还承担一部分负荷,节点5和节点6是纯负荷节点。24小时的总负荷从凌晨的295MW到晚高峰的600MW之间变化。
热备用需求设为120MW。这个数值不是拍脑袋定的——在只有4台机组的小系统中,最大单机容量是300MW,按规程热备用应该接近这个量级,但真按300MW来算的话,整个算例几乎任何时段都要把所有机组全开,反而看不出优化的意义。所以我把需求调到120MW,既能让备用约束在某些时段改变开机方案,又不至于让模型失去灵活性。实际工程中热备用需求一般是根据最大单机容量和负荷预测偏差综合计算出来的。
为了对比效果,我跑了三个场景:场景A是基础机组组合,只考虑功率平衡和机组自身约束;场景B在A的基础上加入热备用约束;场景C在B的基础上再加入线路潮流约束,也就是完整的SCUC模型。
4.2 加不加热备用约束,差别有多大
先看场景A的结果。凌晨谷荷时段,负荷只有300MW出头,G1单机组出力就能满足功率平衡,虽然G1的最小出力是60MW,但只要让它发到300MW就够负载了。但是问题来了,G1满发时没有剩余容量,一旦它跳闸,整个系统立刻失去全部电源。这就是没有热备用约束时方案“经济但危险”的地方。
场景B加上了120MW热备用约束之后,情况立刻不一样了。凌晨时段还要再开一台机组,哪怕这台机组只需发很小的功率,也要维持在线状态。为什么?因为G1如果发到接近满负荷,它能提供的备用容量只有区区几十兆瓦,不满足120MW的要求。只有另一台机组在线运行,才能贡献充足的备用空间。G4虽然启动成本低、容量小,但它的爬坡能力只有50MW/h,燃料成本又高,所以在凌晨通常选择G2作为在线备用机组。
从成本数字上看,场景B比场景A的日运行成本高出了一截,多出来的主要是第二台机组的燃料成本和启动成本。这其实就是“可靠性是有代价的”在数学优化中的直接体现。不承担这部分成本,系统就在拿安全开玩笑。
我还特意看了场景B中每个时段的备用分配明细。在一部分负荷适中的时段,系统把G1的出力压低到250MW左右,G2发到100MW附近,两台机组的可上调空间各贡献一部分,凑够120MW备用。这说明热备用约束并不是简单“多开一台机组”就能满足,还存在一个最优出力分配问题——哪台机组让出多少发电空间,让总成本上升最小,这是优化算法应该自动解决的。
4.3 加不加网络约束,方案怎么变
场景C加入直流潮流约束之后,出现了一个很有意思的现象:在某些高峰时段,G1的出力被压下来了,改由靠近负荷中心的G4多发电。原因很简单,G1位于节点1,它发的电要通过线路L1和L2送到节点4、5、6的负荷区。在没有网络约束的场景B中,G1满发300MW,大量功率通过L2(容量180MW)外送,结果L2严重越限。
网络约束一加上,优化算法不得不重新分配各台机组的出力。G1出力被限制,G3和G4的出力增加。G4虽然燃料成本高,但它就在节点4,发电可以直接供给本地负荷,几乎不占用输电通道,因此在网络阻塞时段反而成了“香饽饽”。这就是SCUC的价值所在:它不会让计划在纸面上很漂亮,执行起来却到处碰壁。
从三条线路的潮流数据可以看到,加了网络约束之后,所有线路的潮流都被控制在容量限值之内,L2留有一定的安全裕度。整体运行成本自然也跟着涨了。我记得场景C的总成本比场景A高了大约6%到8%,这个涨幅对于确保输电安全来说,完全在可接受范围内。
5. 新手最容易踩的坑与调试心得
5.1 求解器接口与数值问题
用YALMIP有个习惯要养成:在求解之前先确认求解器有没有正确识别模型类型。对于纯线性目标+线性约束+二进制变量的MILP问题,YALMIP会调用intlinprog或Gurobi;但如果模型中不小心写入了非线性表达式,YALMIP会尝试调用fmincon等非线性求解器,求解效率和稳定性都差很多。我的做法是,在求解后检查sol.info,看是否返回了'Successfully solved',如果不是,就把sol.info的内容打出来仔细看。
数值问题在机组组合模型中也很常见。大M参数的选取是一个经典难题:取得太大,约束中的数值尺度差异过大,求解器容易陷入数值不稳定;取得太小,又可能把有效约束错误地放宽。我在爬坡约束中把大M取为机组的Pmax,因为Pmax本身比爬坡速率大一个量级,足够“放松”停机时段,又不至于带来数值问题。读者如果遇到求解器报错“infeasible problem”,可以检查大M项是否写反了方向。
5.2 约束建模的高频错误
第一个高频错误:启动事件变量定义不完整。如果只写了su(t) >= u(t)-u(t-1),而不补充su(t) <= u(t)和su(t) <= 1-u(t-1),那么su(t)可能被松弛成一个较大的值,只有下限约束。虽然目标函数里启动成本会压低su(t),但在某些情况下,尤其是启动成本为0时,su(t)的取值可能变得任意,导致最小启停时间约束失去意义。
第二个高频错误:爬坡约束里忘记乘u(t)或u(t-1)。这个错误会导致机组在停机状态下也被分配一个出力变化量,逻辑上说不通。我一开始在写备用约束时也犯过类似的错误,结果求解出的备用容量出现在停机的机组上,后来把r(t,i) <= RU(u)加上之后才修正过来。
第三个高频错误:热备用需求方向写反。有的同学会把Σr(t) >= R写成<=或者==,导致系统“故意”少留备用,这在优化里属于典型的约束方向错误。检查的时候把value(r)打印出来,逐时段看是否满足备用需求,一眼就能发现问题。
第四个高频错误:直流潮流中节点注入向量和支路关联关系搞混。潮流的正方向定义了线路“从哪头流向哪头”,虽然从优化求解的角度看,潮流正负只影响约束的方向,但如果不统一,支路潮流的上限约束就可能形同虚设。每建完一个模型,我都会随机取一个时段,用手动计算的支路潮流验证一下Pflow的值。
5.3 结果合不合理,用这几条自检
经过多次调试,我总结了一套机组组合结果的自检清单,每次跑出新方案都会逐条检查,这里分享给读者:
| 检查项目 | 判断标准 | 常见错误表现 |
|---|---|---|
| 功率平衡 | 每时段各机组出力之和等于负荷 | 总出力不等于负荷,模型无解或漏写约束 |
| 出力范围 | 运行机组出力在Pmin和Pmax之间,停机机组出力为0 | 停机机组有正出力 |
| 热备用 | 每时段可用备用不低于系统需求 | 备用为负、备用集中在停机机组 |
| 爬坡 | 相邻时段出力差不超过爬坡速率 | 出力在启停瞬间出现不合理跳变 |
| 最小启停时间 | 机组开机时长不小于规定值 | 机组一天内频繁启停 |
| 线路潮流 | 所有支路潮流在容量限值内 | 某条线路潮流越限被忽略 |
| 启动成本 | 启动事件次数与机组状态变化一致 | su(t)=1但u前后没有变化 |
这套清单我用了很久,可以说救了我无数次。很多时候模型“跑通”了,但结果不用看也知道有问题,比如机组在半夜启停——这种方案上报的话是没用的。
最后再分享一点实际操作的体会
机组组合这个方向,代码实现从来不是最难的环节,真正难的是把每一个约束的物理含义搞清楚,再转换成正确的数学表达式。我在跑通这个基础版SCUC模型之后,又做了几个方向的扩展:增加N-1安全约束(预想故障集)、把热备用需求改成动态变化的、在目标函数中加入新能源出力的不确定性惩罚项。每扩展一个功能,都要回到最基础的模型逻辑上去审视,看新增约束会不会和原有约束冲突、会不会导致求解困难。
如果你想在这个代码基础上继续深入,建议先尝试把网络约束从直流潮流升级成线性化的交流潮流,再引入储能设备的多时段耦合约束。这些扩展本质上还是在机组组合这个基本框架上叠加更多的复杂约束层,核心的建模思路、求解框架和我上面讲的完全一致。先把这篇文章里的基础模型吃透,后面再去研究那些进阶版本,就会轻松很多。