这两年新能源装机一路狂飙,微电网、综合能源系统、园区级能碳平台这类项目越来越多,但真正动手做调度优化的时候,很多人会发现一个问题:风光出力不稳定、储能充放电时机难判断、碳排放约束一加进来机组组合就变复杂,传统靠经验排产的思路根本撑不住。我这次用Matlab完整做了新能源系统低碳优化调度实战,包含了数学模型搭建、Yalmip建模、求解器调用、结果分析和碳价灵敏度测试,整套流程跑下来,对“绿电怎么用才划算”这件事才算真正有了体感。
这套内容适合谁?电气工程、控制工程、能源方向的研究生,做综合能源或微电网课题的工程师,还有想从纯理论走向代码实现的初学者。我会尽量把数学模型和代码串起来讲,参数也给出可参考的设定,只要你手里有Matlab基础授权,照着改参数就能跑通一个简化版的低碳调度模型。先说明白,我这里聊的是技术和建模方法,软件的安装授权请优先用学校或公司的正版License,别在破解下载上浪费时间。
1. 项目整体思路与设计拆解
1.1 低碳调度到底在优化什么
很多刚接触的人会把低碳调度和经济调度混为一谈,其实两者核心差别就在目标函数的组成上。传统经济调度只算钱:燃料费、运维费、购电费、弃风弃光惩罚,最后得到一个总成本最小的出力计划。低碳调度是在这个基础上把“碳排放量”也变成一个可量化的优化目标,常见做法有两种,一种是直接给碳排放加一个惩罚系数换算成成本,另一种是引入碳交易机制,让碳排放配额和买卖碳配额的费用进入目标函数。
我这次采用的是“碳排放成本化”的思路,也就是把每一时刻系统产生的CO2排放量乘以碳价,加进总成本里。这样做的好处是模型结构简单,和原有的经济目标天然兼容,能直接复用常规优化求解器。碳价一调高,模型自然就会倾向减少火电出力、增加储能放电和新能源消纳比例,这就实现了“低碳”效果。
理解了这个核心,后面很多参数设置就顺了。比如为什么碳价要设成变量而不是固定值,因为我们要做灵敏度分析,观察不同碳价水平下调度行为的变化;为什么目标函数里要有弃风弃光惩罚项,因为模型在极端情况下会选择丢掉新能源来避免储能过充或线路越限,如果不加惩罚,结果会出现“明明有绿电却不用”的反直觉现象。
1.2 为什么选Matlab而不是Python
这个问题我经常被问到。Python在机器学习生态上确实强,但在电力系统优化的传统阵地,Matlab依然有它不可替代的舒适区。首先是Yalmip这个建模工具箱,它能把复杂的优化问题用接近数学公式的写法描述出来,不需要手动把约束展开成标准矩阵形式,这是Matlab生态里最让人省心的一点。Python里的PuLP、Pyomo也能做,但语法习惯和模型调试体验因人而异,团队如果有老代码沉淀,Matlab的延续性优势会很明显。
其次是矩阵运算和内置求解器的联合使用。调度问题本质是一个带约束的数学规划问题,Matlab自带的linprog、quadprog、intlinprog在中小规模问题上足够拿来验证算法,而当你需要更高性能时,Yalmip可以无缝切换Cplex或Gurobi,同一套模型代码基本不用重写,换个求解器名称就行。这种“从入门到工业级”的平滑过渡,是我选择Matlab的最大理由。
我不否认Python在深度学习接入方面的优势,但这篇博文聚焦的是优化调度主体,Matlab搭配预测数据完全够用。如果后续你要做强化学习调度或大规模数据驱动,再考虑换Python也不迟,没必要一开始就陷入语言之争。
1.3 系统组成与典型场景设定
为了把问题限定在一个能跑起来、又不失一般性的范围内,我设计了一个典型园区微电网系统,包含四类核心单元:
- 风电(WT):出力具有随机性,使用典型日预测曲线。
- 光伏(PV):昼间出力明显,夜间为零,使用典型日预测曲线。
- 储能(ESS):磷酸铁锂电池,有容量上限、充放电功率限制和效率损耗。
- 分布式火电机组(柴油机/燃气轮机):可控但会产生碳排放,承担调峰兜底角色。
系统与上级电网存在功率交换,可以买电也可以卖电,大电网的购电价格采用分时电价,同时购电对应的间接碳排放也计入总量。负荷侧简化为一个刚性负荷序列,先不做需求响应,保持模型聚焦。调度周期选24小时,时间步长1小时,这个粒度对初版验证足够,也能清楚看到储能在峰谷时段的充放动作。
这套基准场景的好处是结构简单但要素齐全,能覆盖新能源波动、储能调度、碳约束、多源协调这几个关键点。等你把这份代码吃透,再扩展到多机组、多储能、热负荷耦合,就是改矩阵维度的事,模型框架不用推翻重来。
2. 数学模型搭建:低碳调度的核心细节
2.1 目标函数怎么设计才算“低碳”
先给出我这次使用的目标函数:
min F = Σ_t [ C_fuel(t) + C_om(t) + C_grid(t) + C_ess(t) + C_em(t) + C_curt(t) ]逐项解释。C_fuel是火电机组的燃料费用,用二次函数近似,在简化模型里也可以线性化处理,避免求解器处理非线性项。C_om是各设备运行维护费用,通常按电量比例估算:风机光伏运维成本很低,储能运维成本按充放电电量计,火电运维成本按出力计。C_grid是购售电费用,购电为正、售电为负,但为了鼓励就地消纳,我一般不设置售电收益为正的大头,否则模型会倾向于疯狂买低价电再高价卖出,这种套利行为虽然数学上合理,却不完全符合工程实际。
C_em是整个模型低碳特性的核心项,公式是:
C_em(t) = λ_co2 × (火电排放强度 × P_fuel(t) + 电网间接排放强度 × P_buy(t))这里λ_co2就是碳价,单位元/吨CO2。火电排放强度根据机组燃料类型查表计算,电网间接排放强度可以参考区域电网基准线排放因子。这里有个细节值得注意:如果把碳成本放进目标函数,火电的边际发电成本就会上升,模型会在多个可行方案之间重新排序,有时候哪怕火电发电成本低,但因为碳排放高,整体排名会落后于“绿电+储能”组合。
C_curt是弃风弃光惩罚项,数值设定要大于正常发电收益,否则模型会随意丢弃新能源。我在实践中通常设为正常上网电价的2倍以上,具体值根据电价水平调整。
让我用一个具体数字感受一下。假设碳价50元/吨,火电排放强度0.8kg/kWh,那么每发1kWh火电就要承担0.04元碳成本,折算到日电量的量级里,足够改变调度策略的排序。这就是低碳调度不需要单独设定“减排目标约束”也能起作用的原因:只要碳价给得合适,减排是经济优化自然涌现的结果。
2.2 约束条件的几个“暗坑”
约束条件分四类,每类都有值得细说的点。
第一类是功率平衡约束,也是最容易理解的:任意时刻,风光出力加火电出力加储能放电加购电,必须等于负荷加储能充电加售电。这个约束用等式表示,但它也是整份模型里最“脆”的,数据稍微对不上就会出现不可行解。我习惯在求解之前先做一遍数据校验,把全天总负荷、新能源总可发量、储能容量上下限大致过一遍,心里有数再丢给求解器。
第二类是机组出力约束与爬坡约束。火电出力有上下限,相邻时刻出力变化不能超过爬坡速率。对于没有启停变量的简化模型,爬坡约束是连续性的主要约束。我刚做的时候漏了爬坡约束,结果出来的出力曲线像锯齿一样上下跳,工程上根本无法执行。
第三类是储能约束,这是整个模型里最容易翻车的地方。储能SOC动态约束:
SOC(t+1) = SOC(t) + P_ch(t) × η_ch × Δt - P_dis(t) / η_dis × Δt同时每个时刻的充放电功率有上限,SOC也有上下限。这里有两个暗坑:一个是充放电状态互斥,如果要严格建模,需要引入二进制变量,变成混合整数规划;另一个是终端SOC约束,通常要求一天结束后SOC回到初始值附近,这是为了让储能具备日循环能力,否则模型会把储能在最后时刻放空。
第四类是碳排放约束,如果采用“碳排放成本化”思路,这类约束以目标函数形式存在,不需要单独设硬约束。如果你用的是“碳配额”模式,那就需要增加汇总约束,比如一天总碳排放量不能超过配额值,这属于硬约束,会让可行域缩得很厉害,求解器报错概率也直线上升。新手阶段建议先用碳价软约束,跑通后再尝试配额硬约束。
2.3 风光不确定性怎么纳入模型
上面讨论的都是确定性模型,也就是风光出力直接采用预测曲线。实际中预测误差是客观存在的,处理手段主要有三类。
第一类是场景法,用历史数据生成多个典型风光场景,每个场景有概率权重,优化目标是期望成本最小化。这个思路能保留线性模型的优点,只是变量维度成倍增加,适合入门后第一阶段的扩展。第二类是鲁棒优化,考虑最坏情况下的调度方案可行性,适合对供电可靠性要求高的场景,但结果通常偏保守,经济性会打折扣。第三类是滚动优化,用实时预测数据每隔15分钟或1小时重新求解一次,把不确定性通过高频率刷新来稀释。
我这次实战用的是确定性预测曲线,但在代码架构上预留了场景接口。实际操作中我建议新手先把确定性模型吃透,因为场景法的本质就是在确定性模型外面套一层循环,确定性模型的约束写不对,场景再多也白搭。
3. Matlab实现全流程实操
3.1 环境准备:工具箱与求解器配置
这次实战我用的环境是Matlab R2024a(更新的2025/2026版本功能差异不大,不影响代码),核心工具箱是Optimization Toolbox和Yalmip。Yalmip是第三方面向对象建模工具箱,不是MathWorks官方产品,但学术界和工业界用得非常多。另外我还装了Cplex求解器,用来求解带二进制变量的混合整数规划问题。
没有Cplex也不要紧,Yalmip自带回调可以调用Matlab内置的intlinprog,只是求解速度会慢一些,24时段的模型一般也能在几十秒内完成。如果你要做96时段或者多场景扩展,那Gurobi或Cplex基本是必选项,差异在分钟级和秒级之间,体感非常明显。
配置Yalmip的步骤是先解压工具箱,然后 addpath 到路径,再用 savepath 保存。然后运行 yalmiptest 验证,能通过就说明求解器接口没问题。这些操作网上官方文档很详细,我就不逐字写了,只提醒一点:Yalmip官方建议用安装包自带安装脚本,直接双击运行 InstallYALMIP.m 就行,不要手动把整个文件夹加进路径,容易造成函数冲突。
3.2 数据准备与模块化编程
模型跑不跑得通,一半取决于数据整理得干不干净。我习惯用Excel或者CSV存参数,Matlab脚本里用readtable读取,而不是在代码里硬编码一大串数字。数据分为几块:负荷曲线、风电出力系数、光伏出力系数、分时购电价、碳价、设备参数。
设备参数包括储能容量、初始SOC、SOC上下限、充放电效率、充放电最大功率、火电出力上下限、爬坡速率、燃料成本系数、排放强度等。建议把参数集中定义在一个结构体里,比如 params.ess.capacity = 1000;,这样后面改参数不用翻代码,模型代码也更干净。
模块化编程上,我把整个项目拆成四个脚本:main.m负责读数据和设置参数,model_build.m负责定义变量和约束(核心),solve_model.m负责调用求解器并提取结果,plot_result.m负责画图和输出报表。这样的好处是每个脚本职能单一,出了问题定位很快。我最开始把所有代码堆在一个文件里,两百多行找一行约束能找半个小时,后来老老实实拆开,调试效率翻倍。
3.3 核心代码框架与求解流程
这里给出一个简化但完整的Yalmip建模示例,注意为了可读性我压缩了部分循环,实际工程代码请按模块拆分。
%% 参数定义(示意) T = 24; % 时段数 dt = 1; % 步长,小时 load_profile = [ ... ]; % 1x24 负荷曲线 wind_profile = [ ... ]; % 1x24 风电出力系数 pv_profile = [ ... ]; % 1x24 光伏出力系数 price_buy = [ ... ]; % 1x24 购电价 lambda_co2 = 50; % carbon price,单位 元/吨 %% 决策变量 P_w = sdpvar(1, T, 'full'); % 风电实际消纳功率 P_pv = sdpvar(1, T, 'full'); % 光伏实际消纳功率 P_fuel= sdpvar(1, T, 'full'); % 火电出力 P_buy = sdpvar(1, T, 'full'); % 购电 P_sell= sdpvar(1, T, 'full'); % 售电 P_ch = sdpvar(1, T, 'full'); % 储能充电功率 P_dis = sdpvar(1, T, 'full'); % 储能放电功率 SOC = sdpvar(1, T+1, 'full'); % 储能SOC状态 %% 约束 Constraints = []; % 出力上下限 Constraints = [Constraints, 0 <= P_w <= wind_profile .* wind_capacity]; Constraints = [Constraints, 0 <= P_pv <= pv_profile .* pv_capacity]; Constraints = [Constraints, fuel_min <= P_fuel <= fuel_max]; Constraints = [Constraints, 0 <= P_buy <= buy_max]; Constraints = [Constraints, 0 <= P_sell <= sell_max]; % 爬坡约束 for t = 1:T-1 Constraints = [Constraints, -ramp_rate <= P_fuel(t+1) - P_fuel(t) <= ramp_rate]; end % 储能约束 SOC(1) = 0.5 * ess_capacity; for t = 1:T Constraints = [Constraints, SOC(t+1) == SOC(t) + P_ch(t)*eta_ch - P_dis(t)/eta_dis]; Constraints = [Constraints, 0 <= P_ch(t) <= ch_max * u_ch(t)]; Constraints = [Constraints, 0 <= P_dis(t) <= dis_max * u_dis(t)]; Constraints = [Constraints, u_ch(t) + u_dis(t) <= 1]; end Constraints = [Constraints, SOC(T+1) == SOC(1)]; % 终端SOC约束 Constraints = [Constraints, 0 <= SOC <= ess_capacity]; % 功率平衡 Constraints = [Constraints, P_w + P_pv + P_fuel + P_dis + P_buy == load_profile + P_ch + P_sell]; %% 目标函数 Objective = sum(fuel_cost_coef .* P_fuel) ... + sum(om_cost_w .* P_w + om_cost_pv .* P_pv) ... + sum(price_buy .* P_buy - price_sell .* P_sell) ... + sum(lambda_co2 * (fuel_emission .* P_fuel + grid_emission .* P_buy)) ... + curtail_factor * sum((wind_profile .* wind_capacity - P_w) + (pv_profile .* pv_capacity - P_pv)); %% 求解 ops = sdpsettings('solver', 'cplex', 'verbose', 2); optimize(Constraints, Objective, ops);这段代码里有几个地方需要特别说明。
P_w和P_pv的上限用的是预测可发功率乘以装机容量,实际消纳量不能超过它,这样弃风弃光量就可以通过上限减去实际值来量化。u_ch和u_dis是二进制变量,定义了充放电互斥关系,所以这个模型实际上是混合整数规划,用Cplex求解。如果你去掉储能互斥约束、不设机组启停变量,那整个问题退化成线性规划,Matlab内置linprog就能直接求解。
功率平衡约束我写成了等式,负荷加上充电需求必须等于所有供电之和。特别注意P_buy和P_sell理论上不应该同时大于0,严格建模也需要加互斥变量,不过因为分时电价设计的合理性,模型通常不会同时买电又卖电,简化处理在工程上问题不大。
优化完成后,用 value() 函数把变量值提取出来,就得到每个时段的出力计划。以下是我提取并检查结果的小脚本:
P_w_opt = value(P_w); P_fuel_opt = value(P_fuel); P_ch_opt = value(P_ch); P_dis_opt = value(P_dis); SOC_opt = value(SOC); disp(['总成本 = ', num2str(value(Objective))]);3.4 结果对比与灵敏度分析
模型跑通只是第一步,关键是结果对不对、有没有洞察。我通常先画三张图:一张是24小时功率平衡堆叠图,一张是储能SOC曲线图,一张是碳排放逐时曲线图。堆叠图能直观看出每个时段由哪些电源在撑负荷,SOC曲线能验证储能“低谷充、高峰放”的预期,碳排放曲线能看出低碳约束起作用的时段。
我这次跑下来的典型结果是:凌晨负荷低谷时段,风电出力相对充足,储能开始充电;白天光伏大发时段,系统优先消纳光伏,火电出力被压到较低水平;傍晚负荷尖峰且光伏归零,储能放电、火电爬坡补充缺口。整个调度在逻辑上是讲得通的。
做方案对比时,我把“传统经济调度”和“低碳调度”各跑一遍,对比两个指标:总成本、总碳排放。测试碳价从0、50、100、200元/吨逐步上调时,总碳排放呈现阶梯式下降,但总成本也在上升。这两者的权衡曲线其实就是碳价策略的物理映射,也是给决策者最有说服力的一张图。我用Matlab的plot函数画出来,很简单,但解释力很强。
这里我建议你把碳价变化时的机组出力也画出来,能看到火电出力阈值式变化的点,这对理解“碳价引导行为”非常有帮助。我从这些实验里得到的直观结论是:碳排放下降带来的收益是边际递减的,碳价过高后储能容量和新能源消纳空间已经用尽,再提碳价只是增加系统成本,不会继续显著减碳。
4. 常见问题与排查技巧实录
4.1 求解报错与不可行问题
我调试过程中遇到最多的一类问题是Yalmip提示“Problem is infeasible”(不可行)。这个问题的根源通常是约束之间互相打架,最常见的原因有三个。
第一个是数据量纲不统一。比如功率用kW,电能用kWh,但储能容量却是MWh,量级差好几倍,SOC都超上限了,约束自然无解。我的经验是开工前先统一单位体系,黄金法则就是“功率×时间=能量”,一遍遍检查SOC动态方程里每个变量的单位。第二个是终端SOC约束和储能初始SOC设置冲突,比如强迫SOC(T+1)等于SOC(1),但一天中净充电量又超过了储能容量,这也会不可行。解决方法是先放松终端SOC约束为区间约束,比如 [0.95SOC_initial, 1.05SOC_initial],商模型调通后再收紧。第三个是功率平衡等式太硬,当风光可发量、负荷、购电上限都给死,又没有给储能足够的调节空间时,等式约束几乎找不到可行解。此时检查储能容量和购电上限是否宽裕,必要时放宽购电上限。
还有一个调试技巧:打开sdpsettings里的debug模式,Yalmip会给出可疑约束的提示,也可以把所有约束拆出来单独检查可行性,逐个测试哪一组约束导致了冲突。这就像排查电路里哪个元件短路,得一个个隔离。
4.2 储能SOC时间耦合的经典坑
储能SOC的约束是跨时段的,SOC_t会影响SOC_{t+1},所以建模时必须把SOC作为决策变量序列,而不是每个时段独立求解。新手最容易犯的错误是把每个时段当成独立的优化问题来解,结果储能策略完全乱套。我早期犯过这个错误,当时结果图里的SOC像跳楼一样毫无连续性,后来意识到SOC必须放在同一个优化问题里跨时段联动。
另一个经典坑是充放电效率的处理方式。如果充电效率和放电效率都集中在SOC更新方程里,注意效率是相乘还是相除。充电时,储能“存入1kWh”需要电网侧输入大于1kWh,所以SOC增量是P_ch × η_ch;放电时,SOC减少量是P_dis / η_dis,也就是说放电1kWh到负荷,电池内部要消耗多于1kWh的能量。这个一乘一除别搞反,我见过不少人把两个效率都放成乘号,结果一天下来能量凭空多出一大截,违反热力学,结果还不显著,很难发现。
还有充放电互斥约束。如果模型允许同时充放电,最优解里极有可能出现“一边充电一边放电”的荒唐行为,因为目标函数里有能量损耗项的时候,求解器会利用对偶差异去“套利”。加上u_ch + u_dis <= 1这个约束,问题立刻消失。这也解释了为什么要用二进制变量,即使你觉得没必要。
4.3 结果异常检查清单
如果模型跑通了,但结果看起来不对劲,我建议按下面这个清单逐项排查。我把它们整理成表格,方便对照。
| 异常现象 | 可能原因 | 检查方法 |
|---|---|---|
| 火电出力恒为0 | 碳价过高或燃料成本系数远高于购电价 | 调低碳价,对比经济调度结果 |
| 储能从不充电 | 购入电价高于充电成本收益,或SOC初值太高 | 检查分时电价和SOC初始值 |
| 弃风弃光严重 | 储能容量不足或惩罚系数设置过低 | 调高弃风弃光惩罚,观察新能源消纳率 |
| 功率平衡不闭合 | 等式约束写错,漏了某个负荷项 | 按小时逐项累加,对照负荷曲线 |
| SOC曲线突变跳变 | 效率公式错误,或SOC上下界太窄 | 检查充电/放电效率在SOC方程中的位置 |
| 总成本为负 | 售电价高于购电价,模型无限套利 | 修正电价数据,保证售电价 < 购电价 |
其中总成本为负这个现象我印象很深。有一次我用一套没有校验过的分时电价数据,某些售电时段价格高于购电时段,模型就开始疯狂低买高卖,最后总成本算出来负几万元,物理上完全荒谬。从此之后我养成了习惯:任何参数进入模型前,先画一条曲线看看趋势是否合理,特别是电价、负荷这类和工程直觉强相关的数据。
5. 后续扩展方向
5.1 预测与调度联动
前面用的是“给定预测曲线”的开环方式,工程上更常见的是把预测闭环起来。比如用历史风电数据训练一个BiLSTM模型预测未来24小时出力,再把预测结果作为调度的输入参数;储能SOC还可以结合电池老化模型做修正。Matlab的Deep Learning Toolbox支持搭建BiLSTM,数据准备、训练、导出预测曲线这一套流程能顺滑接到调度脚本里。把预测误差的分布统计出来,还可以用场景法生成多套随机场景来测试调度方案的鲁棒性。这是低碳调度系统落地时性价比最高的扩展方向。
5.2 多目标与多时间尺度
如果要把“低碳”和“经济”作为两个独立目标,而不是简单加权合成一个目标,可以用多目标优化算法,比如NSGA-II,得到一组Pareto前沿解。Matlab的Global Optimization Toolbox提供了gamultiobj,可以直接调用。另一个方向是多时间尺度滚动优化:日前计划用1小时步长做出力计划,日内每15分钟用短时预测刷新一遍,修正储能和机组出力。这种架构更贴近工程实际,但对数据接口和求解速度都提出了更高要求,建议先把基础模型跑熟再做。
我在实际项目中还有一个体会:调参的过程本身就是理解系统的过程。碳价从20元调到200元,你会眼睁睁看着火电从“主力军”变成“替补队员”,储能从“偶尔动作”变成“每天必动”,这种直观感受比看十篇论文都来得深刻。所以我还是建议刚开始的人不要追求一步到位,先把最简版本跑通,再一步步加约束、换数据、改场景,错得越早,进步越快。