
1. 这个课题到底在解决什么问题源荷不确定性下的低碳调度难题这几年双碳目标带火了电力系统的低碳调度研究但真正动手写过代码的人都知道难点不在低碳二字上而在不确定性这三个字。拿我自己的经历来说最早接手这个题目时我以为只是给传统经济调度加一个碳排放惩罚项就行结果程序跑出来的结果怎么都不对劲——有些场景下机组启停方案特别激进有些场景下备用容量明明充足却大量弃风。后来才意识到问题出在我把所有输入都当成了确定值。在含风电的电力系统里不确定性来自两个方向。电源侧风电出力取决于风速而风速本身是个随机过程一天之内发电功率可以从零跳到额定容量的百分之七八十负荷侧用户的用电行为虽然有一定规律但预测误差随预测时长增加会明显放大尤其在峰谷时段偏差可能达到预测值的5%到10%。这两个不确定性叠加在一起调度方案就不能再做单点估计必须考虑一套决策在多种可能场景下都可运行且够经济。低碳调度的本质是在传统以煤耗成本最小为目标的基础上把碳排放成本内部化。国内目前主流做法是引入碳排放权交易机制政府给发电企业分配碳配额实际排放低于配额的部分可以出售获利超出配额的部分必须到市场上购买或者接受惩罚性碳价。这样一来高碳机组的发电成本上升低碳机组风电、燃气机组的相对经济性提升调度结果自然向低碳倾斜。但问题在于一旦把不确定性考虑进来碳成本的计算也变得不确定——同样是某一时段开机风电出力的高低直接决定了火电机组要发多少电、排多少碳。所以这个课题的核心链路是源荷双侧不确定性建模 → 场景生成与削减 → 含碳交易成本的多目标或单目标带惩罚调度模型 → 混合整数规划求解 → 结果对比分析。每个环节都有独立的坑下面我分块拆解。2. 源荷两侧不确定性的数学建模从概率分布到典型场景2.1 风电出力不确定性分风速和功率两步走风电出力不确定性的建模业界常用的是风速-功率两级转换。风速近似服从威布尔分布Weibull distribution其概率密度函数为[ f(v) \frac{k}{c}\left(\frac{v}{c}\right)^{k-1}\exp\left(-\left(\frac{v}{c}\right)^k\right) ]其中 ( k ) 是形状参数( c ) 是尺度参数。实际做项目时不需要自己拟合这两参数直接参考文献中常给的经验值即可比如 ( k2.2 )、( c8.5 )大概对应的就是平均风速七八米每秒的风电场。有了风速之后通过风机的功率特性曲线换算成出力。常见的简化模型是分段线性[ P_w(v) \begin{cases} 0, v v_{in} \text{ 或 } v \geq v_{out}\ P_r \cdot \frac{v - v_{in}}{v_r - v_{in}}, v_{in} \leq v v_r\ P_r, v_r \leq v v_{out} \end{cases} ]( v_{in} ) 是切入风速、( v_{out} ) 是切出风速、( v_r ) 是额定风速、( P_r ) 是风机额定功率。这套转换逻辑不难但写代码时最容易忽略的是最终的出力场景要叠加一个截断处理。很多初学者直接拿正态分布或威布尔分布采样后套公式却忘了风电出力有上限和下限导致生成的场景里出现负值或超过装机容量的值场景削减后调度模型直接无解。2.2 负荷不确定性正态分布与预测误差负荷侧的不确定性相对好处理通常假设负荷预测误差服从均值为零、标准差为预测值一定比例的正态分布。即实际负荷可以表示为[ P_L(t) \hat{P}_L(t) \varepsilon(t), \quad \varepsilon(t) \sim \mathcal{N}(0, \sigma^2(t)) ]( \sigma(t) ) 一般取预测值的2%到5%我习惯取3%因为再小体现不出不确定性效果再大调度方案会过于保守、经济性损失明显。对于一天24小时的调度周期每个时段都独立采样误差然后叠加到预测负荷曲线上就得到一组负荷场景。有个细节值得注意负荷的真实分布其实更接近截断正态或t分布因为极端偏差的概率被电网运行经验证明是偏低的。但论文里用标准正态分布已经足够除非审稿人特别较真否则不必在这个地方过度设计。2.3 场景生成与削减蒙特卡洛 同步回代削减不确定性处理的思路最终要落在场景这两个字上。所谓场景就是一组同时包含风电出力、负荷水平的时段序列。我做这个课题时用的是蒙特卡洛采样生成大量初始场景然后用同步回代削减法Scenarios Reduction by Simultaneous Backward Reduction把场景数量压缩到计算可接受的范围。具体做法分四步第一步对每个时段的风速和负荷误差分别采样得到一组初始场景。比如一天24个时段每个时段风电和负荷各采800个样本组合起来就是800个完整场景。第二步定义场景之间的距离。常用的是欧几里得距离即所有时段上两个场景对应值的差的平方和。这里注意风电和负荷的量纲一致都是MW可以直接算距离但如果你后续扩展模型加入电价、光伏等不同量纲变量需要先归一化再算距离。第三步用同步回代削减迭代每次找出会使总概率距离损失最小的场景并把它删除同时将其概率加到离它最近的场景上。迭代直到场景数满足预设值我一般削减到10到20个。第四步把削减后的场景及对应概率作为调度模型的输入。这里有个经验谈场景削减不能只追求数量少还要看削减后的场景集能否覆盖极端情况。比如风电出力的高风场、低风场场景都保留至少一个否则削减算法为了总距离最小会把极端场景抹平调度结果在极端天气下完全不靠谱。我自己的做法是削减后再手动检查一遍看最大出力场景和最小出力场景是否还在不在就手动补回。3. 低碳调度模型构建目标函数与约束的取舍3.1 目标函数综合成本最小化低碳调度最常见的目标函数是系统总成本最小总成本由三块构成第一块是火电机组的煤耗成本。一般用二次函数[ C_f(P_i) a_i P_i^2 b_i P_i c_i ]( a_i, b_i, c_i ) 是机组煤耗系数。MILP里二次项不好处理通常用分段线性化逼近。我这里直接取了10段线性化精度已经足够而且求解速度快不少。第二块是碳交易成本。先算系统碳排放量[ E_{total} \sum_{t} \sum_{i \in G} \left( \alpha_i P_{i,t} \beta_i \right) \Delta t - E_{w} ]其中 ( \alpha_i, \beta_i ) 是机组i的碳排放强度系数单位tCO₂/MWh 或 tCO₂/h( E_w ) 是风电的碳排放抵扣一般风电低碳甚至零碳且可冲减一定比例。然后对比碳配额 ( E_{cap} )[ C_{CO2} \begin{cases} \lambda (E_{total} - E_{cap}), E_{total} \geq E_{cap} \text{需要购买配额}\ \lambda \cdot \mu (E_{total} - E_{cap}), E_{total} E_{cap} \text{盈余配额可出售但折价} \end{cases} ]( \lambda ) 是碳交易价格( \mu ) 是出售折扣系数常取0.5到0.8。这里必须说明如果不加这个非对称处理系统会被引向极端低碳而牺牲经济性实际市场中碳价也是有同样机制的。折价系数是为了避免为了卖配额而过度压减火电的不合理行为。第三块是弃风惩罚成本[ C_{curtail} \rho \sum_{t} \left( P_{w,t}^{fore} - P_{w,t}^{sch} \right) ]( \rho ) 是弃风惩罚单价( P_{w,t}^{fore} ) 是风电预测出力( P_{w,t}^{sch} ) 是调度实际消纳的风电功率。加这一项是为了让优化器尽量消纳风电但又不至于为了消纳风电而付出过高的火电调节代价——这是调度问题里常用的柔性约束处理方式。这三项加权求和得到总目标函数。权重怎么定我建议碳价和弃风惩罚单价直接参考实际市场数据不要拍脑袋。比如碳价取25到40美元/吨视研究年份弃风惩罚取100美元/MWh左右足够高但不至于荒谬。3.2 约束条件该卡的卡死该放开的放开约束方面电力系统调度有几条不可违反的硬约束也有可灵活处理的软约束要分清。第一是功率平衡约束[ \sum_{i \in G} P_{i,t} \sum_{w \in W} P_{w,t}^{sch} P_{L,t}^{sch} ]这个约束是等式约束必须严格满足否则系统就不平衡了。注意我这里用的是 ( P_{L,t}^{sch} ) 即调度后的负荷值不是预测值。因为源荷不确定性建模时每个场景下负荷是不同的。第二是机组出力上下限约束[ P_i^{min} \leq P_{i,t} \leq P_i^{max} ]以及爬坡约束[ -P_{i}^{ramp_down} \leq P_{i,t} - P_{i,t-1} \leq P_{i}^{ramp_up} ]爬坡约束最容易在代码里出问题尤其是机组启停状态切换的那个时段启机瞬间引起的出力跳跃可能会违反爬坡限制。解法是引入机组运行状态变量 ( u_{i,t} )0/1变量爬坡约束写成[ P_{i,t} - P_{i,t-1} \leq (P_i^{ramp_up})u_{i,t-1} P_i^{max}(1 - u_{i,t-1}) ]这个式子的逻辑是如果上一时段机组在运行( u_{i,t-1}1 )则爬坡上限生效如果不在运行( u_{i,t-1}0 )则本时段可以自由启动到最大出力。很多人写代码时把爬坡约束写成对所有时段统一的形式结果在机组合并启停的优化结果里出现违例这个坑很典型。第三是旋转备用约束[ \sum_{i \in G} \min\left(P_i^{max} - P_{i,t}, P_i^{ramp_up}\right) \sum_{w} \left( P_{w,t}^{fore} - P_{w,t}^{sch} \right) \geq R_t ]这个约束的含义是系统在时刻 ( t ) 要能应对突发扰动所以所有机组的可调容量之和必须大于备用需求。( R_t ) 的取值一般取当期负荷的5%到10%或者取最大单机容量。因为不确定性场景的存在( R_t ) 可以在每个场景下分别取值这样能够真实反映源荷不确定性对备用需求的改变——风电出力低的场景火电就得多留一些调节余量。第四是碳排放约束。这个有两种写法一种是作为目标函数里的碳交易成本项而不加硬性约束即上面3.1的写法另一种是给系统设一个总碳排放上限强制满足[ E_{total} \leq E_{limit} ]两种都常见区别在于论文的论证偏好。我个人推荐先做目标函数里的碳交易成本再在敏感性分析部分对比加上硬约束的结果这样能直观展示碳价机制和碳限额机制的区别审稿人也喜欢看这个对比。3.3 场景约束的合并形式鲁棒与随机的折中当有多个场景时约束怎么合并标准做法是对于每个场景功率平衡、旋转备用、爬坡约束都需要分别满足。机组出力和启停状态变量要区分第一阶段变量和第二阶段变量。第一阶段变量是预调度变量日前决策在不确定性实现前就要确定——典型的是机组启停状态 ( u_{i,t} )第二阶段变量是再调度变量实时调整可以在场景实现后调整——典型的是机组出力 ( P_{i,t,s} )。这种两阶段结构在数学上叫带补偿的随机规划MILP的规模会随着场景数和时段数急剧膨胀。假设10个场景、24个时段、6台机组变量数和约束数就在几千量级Gurobi还能轻松解。如果场景数上到50求解时间会明显变长这时候就需要取舍。我在代码里默认支持的场景数是10这也是论文里最常出现的数据量。4. 求解工具选型与Matlab代码架构4.1 为什么选Yalmip Gurobi而不是纯MatlabMatlab里求解混合整数线性规划原生可以用intlinprog但这个求解器面对几百上千个整数变量就力不从心了尤其在多场景MILP里求解时间能拖到半小时以上。我在这个课题里用的是Yalmip接口加Gurobi求解器。Yalmip是一个Matlab的建模语言它最大的价值是让建模和求解分离——你用人类可读的代数符号写约束和目标然后指定用哪个求解器Yalmip负责翻译成求解器能吃的形式。这个设计让调试速度快很多因为你可以先写小规模问题验证模型正确性再扩大场景规模而不需要来回换代码框架。Gurobi的MILP求解性能在学术圈是公认的标杆对问题规模和整数变量数量的承受力比intlinprog强一个量级。不过Gurobi是商业软件教育版需要申请license。如果没有Gurobi退而求其次可以用CBCfree或Matlab自带的intlinprog但在场景数超过20的情况下我不太推荐intlinprog。4.2 代码文件结构模块化是调试的救星我的Matlab代码架构分六个模块每个模块单独一个.m文件主脚本只负责按顺序调用case_data.m系统参数包括机组参数、负荷数据、风电场参数、碳交易参数。所有数据集中在尾部的一个大struct里方便批量修改。generate_scenarios.m根据不确定性模型生成并削减初始场景输出场景集和对应概率。reduce_scenarios.m实现同步回代削减算法输入初始场景集输出削减后的场景集和概率索引。build_scheduling_model.m搭建Yalmip模型把目标函数和约束条件全部实例化。solve_and_postprocess.m调用求解器求解输出机组出力时序、风电消纳情况、碳成本构成等关键数据。plot_results.m绘制机组出力堆叠图、风电消纳图、碳排放柱状图、场景对比图。模块化的好处是如果结果有问题你可以单独验证某一部分。比如怀疑场景削减有bug就跑一遍generate_scenarios.m看削减前后的总距离是否合理递减怀疑碳交易成本算错就把目标函数里其他项先注释掉看单独碳成本项是否按预期变化。这种逐步排查的习惯远比我一开始写一个大脚本从头到尾的方式高效。4.3 Yalmip建模核心代码逻辑搭建调度模型的核心代码骨架大概长这样省略参数定义%% 定义变量 P sdpvar(n_gen, n_horizon, n_scenario); % 火电机组出力 u binvar(n_gen, n_horizon); % 机组启停状态第一阶段 Pw sdpvar(n_wind, n_horizon, n_scenario); % 风电消纳功率 % 还有碳交易相关的连续变量等 %% 目标函数 objective 0; for s 1:n_scenario for t 1:n_horizon % 煤耗成本分段线性化后可以直接线性求和 objective objective prob(s) * (C_fuel(P(:,:,s), t)); % 碳交易成本 objective objective prob(s) * C_carbon(P(:,:,s), t); % 弃风惩罚 objective objective prob(s) * C_curtail(Pw(:,:,s), t, Pw_fore(:,:,s)); end end %% 约束 constraints []; for s 1:n_scenario for t 1:n_horizon constraints [constraints, sum(P(:,t,s)) sum(Pw(:,t,s)) P_load(t,s)]; % 出力上下限、爬坡约束带启停状态、备用约束、碳约束等 end end %% 求解 optimize(constraints, objective, sdpsettings(solver, gurobi, verbose, 2));这里有个容易忽略的坑目标函数里的机组煤耗成本二次项Yalmip可以直接写二次函数并用Gurobi的非凸MIQP求解但求解速度惨不忍睹。更好的做法是自己写分段线性化函数把二次项转化为线性项整个模型变成MILP。Gurobi解MILP的速度比MIQP快十倍不止尤其在场景多的时候。我在代码里早就封装了一个linearize_cost.m输入煤耗系数和分段数输出线性化后的各段斜率和截距运行效率高很多。4.4 场景削减代码的细节实现同步回代削减的代码不复杂但要注意数值精度问题。核心循环是while num_active target_num min_dist_loss inf; k_star 0; for k 1:num_active % 找离k场景最近的另一个场景 dist_to_others sqrt(sum((scenario_set(:,k) - scenario_set).^2, 1)); dist_to_others(k) inf; % 排除自身 [min_dist, nearest_idx] min(dist_to_others); % 计算删除k场景带来的概率距离损失 dist_loss prob(k) * min_dist; if dist_loss min_dist_loss min_dist_loss dist_loss; k_star k; nearest_star nearest_idx; end end % 删除k_star概率加到nearest_star上 prob(nearest_star) prob(nearest_star) prob(k_star); scenario_set(:, k_star) []; prob(k_star) []; num_active num_active - 1; end这段代码最耗时的是计算所有场景两两之间的距离矩阵数据量大时O(n²)的复杂度会拖慢整体。优化方法是用大循环加向量化避免内层套一个小循环。我在我的实现里用pdist2函数配合矩阵索引一次算完所有距离再从距离矩阵里贪婪筛选速度提升明显。还有个小陷阱prob和scenario_set在循环里不断删行初始索引一定要备份否则削减完你就不知道剩下的场景对应原始的风速序列还是负荷序列了。我在结构体里加了original_idx字段专门记录每个存活场景对应原始数据集合中的哪一组这样后续分析结果时能回溯到具体场景的物理含义不至于拿着一个不知从哪来的场景做图表。5. 结果分析与敏感性验证跑出图之后还能看到什么5.1 不同场景下的调度结果差异模型跑通之后第一件该做的事是对比确定性模型和不确定性模型的结果差异。做法很简单把源荷两侧的预测值当作真实值只保留一个场景即确定性场景求解一次再拿10个场景的随机规划模型求解一次对比两者的总成本、碳排放量、风电消纳率。我做过的实验里确定性模型给出的总成本通常比随机规划低5%到10%但碳排放量却往往更高。原因倒不复杂确定性模型有信息优势相当于提前精确知道风电出力可以把火电出力压得更低、碳成本更少。但现实世界中风电出力不可能被精确预测一旦实际风电与预测偏差大确定性模型给出的调度方案就需要大量弃风或强制启动高碳机组来补救实际碳排放反而上升。把这个对比写进论文里比空口强调不确定性很重要有说服力得多。5.2 碳价与弃风惩罚系数的影响碳价是低碳调度里最核心的杠杆参数。我建议做一个碳价敏感性分析把碳价从10元/吨依次提高到100元/吨看系统总碳排放量和总成本怎么变化。明显的趋势是碳价低时系统会倾向于多发火电、少用风电调节因为风电的不确定性在低碳价下不值得用高成本的火电备用去消化碳价提高后调度系统愿意付出更多火电爬坡成本来消纳风电碳排放量逐渐下降。这个拐点往往出现在碳价与火电煤耗成本、弃风惩罚成本三者的平衡点附近。另一个值得测的参数是弃风惩罚系数。如果这个系数设置得太低调度模型会倾向于大量弃风来省去火电调节的麻烦风电消纳率很难看如果太高又会强迫系统接受所有风电哪怕要火电疯狂爬坡总成本高得离谱。合理的区间要让系统弃风率落在5%到10%的自然水平而不是人为强迫到零。5.3 求解时间与场景数的权衡10个场景下Gurobi求解时间一般在几十秒到几分钟的量级完全可接受。如果加到30个场景求解时间可能会膨胀到半小时以上。我试过把场景数加到50求解时间直接奔着两个小时去了而且解的改善幅度非常有限。效率与精度的平衡点我建议做研究时用10个场景做基准实验在敏感性分析里可以单独测一次20个场景说明结果对场景数不敏感即可没必要追求大规模场景。6. 复现这个课题时的几处关键避坑点6.1 参数单位与量纲的统一这个项目里最容易翻车的不是数学建模而是参数单位。煤耗系数的单位可能是元/MWh²或元/MWh碳排放强度的单位可能是tCO₂/MWh或kgCO₂/MWh碳价的单位更是千差万别——有按元/吨、按美元/吨、按元/kg的。稍不留神目标函数里碳交易成本和煤耗成本就差了三个数量级优化结果完全跑偏。我踩过最狠的一次坑从一篇论文里抄了碳排放强度系数没注意它用的单位是kg/MWh而我前面的煤耗成本和碳价都按吨来算结果碳交易成本比煤耗成本小了1000倍系统根本不在意碳排放优化结果跟普通经济调度没区别。所以拿到任何数据第一步统一单位制第二步写个简单的unit_check.m脚本打印出各项成本的中位数量级是否合理比人工肉眼检查可靠得多。6.2 场景削减后概率和为1的校验场景削减过程中每删除一个场景都要把概率转移给最近邻场景如果算法实现有误或者浮点误差累积削减完的概率数组可能不为1。这个看似小问题在Yalmip里会表现为约束数值奇异或目标函数不一致。我养成的习惯是每次削减完立刻assert(abs(sum(prob) - 1) 1e-8)校验一次不通过马上打印日志定位哪一步概率转移出错。6.3 Yalmip模型中的sdpvar维度一致性Yalmip建模时候最让人头大的错误是变量维度不匹配。尤其当P定义成(n_gen, n_horizon, n_scenario)而约束里误写成sum(P(:, t))只取第一个场景时Yalmip不会直接报错而是自动广播维度导致约束池异常庞大、求解器疯狂报infeasible。我建议在每个约束写完后用一个assert(length(constraints) expected_num)的检查来拦截维度异常虽然不够智能但至少能快速缩小排查范围。6.4 两阶段变量结构的时段索引错误机组启停变量是第一阶段而出力变量是第二阶段分场景调整。这意味着启停决策在场景实现前就定了各场景下同一机组同一时段的启停状态必须一致。很多初学者在写约束时把启停变量也标成场景相关结果允许了每个场景有不同的启停方案不仅求解规模爆炸而且跑出来的调度结果在物理上不可执行。我在代码里刻意把u声明为binvar(n_gen, n_horizon)而不是binvar(n_gen, n_horizon, n_scenario)从变量声明层面杜绝了这个错误建议你也这样写。7. 扩展思路从本课题还能往哪里走这个模型做完后如果想继续深化或改造成自己的论文方向有几个现成的扩展点。第一个是引入考虑储能或需求响应的低碳调度。源荷不确定性在处理时本质上是给系统加灵活调节资源。储能的充放电决策天然是两阶段变量的一部分——日前不确定时定好储能基准充放电计划日内根据实际出力和负荷偏差实时充放电正好匹配随机规划框架。需求响应同理把可中断负荷和可转移负荷建模为虚拟机组它们的调节范围就是负荷侧的额外灵活性能显著降低备用需求。第二个是把场景法换成分布鲁棒优化。场景法的局限在于你需要知道精确概率分布但实际中风速分布参数本身就有不确定性。分布鲁棒优化把概率分布本身当作不确定集用模糊集约束所有可能的分布得到的调度方案鲁棒性更强但建模难度和求解复杂度也会上一个台阶。如果审稿人要求方法有创新性分布鲁棒是个不错的方向。第三个是把碳排放流模型引入。目前这个模型是总量碳交易模式即用系统总碳排放量乘碳价。但如果要更精细地分析碳排放在哪一个机组、哪一个时段、因为哪一个负荷而产生就得用到碳排放流理论把碳排放责任分摊到负荷侧。这个方向这两年论文产出很多适合做深度扩展。我个人实际做这个课题最大的感受是写代码从能跑通到结果合理中间隔着的不是数学问题而是对物理问题理解得透不透。源荷不确定性、低碳调度每一个名词背后都对应着一堆现实约束。把这个代码从头到尾调试一遍远比背十篇论文更能帮你建立对电力系统调度问题的直觉。如果碰到哪里解不出来或者结果不对劲欢迎在评论区交流踩过的坑值得拿出来分享。