1. 项目整体设计与思路拆解
做热电联供微网优化的朋友,估计都遇到过同样的问题:风机和光伏的出力一会儿高一会儿低,电负荷和热负荷也不是固定值,如果拍脑袋取个平均值去调度,轻则经济性差,重则系统失稳。我这次做的这个“考虑源荷随机特征的热电联供微网优化研究”,就是用Matlab把这一整套不确定性建模、场景生成、优化调度、结果分析全部串起来的项目,重点解决“源荷随机特征怎么进模型”和“优化模型怎么建、怎么写代码”两件事。整套代码用Yalmip搭建MIP模型,调用商业求解器求解,适合正在做综合能源、微网调度方向研究的人参考,也适合想从确定性模型过渡到随机优化模型的初学者。
先说整体设计思路。热电联供微网里通常包含风力发电、光伏、燃气轮机或内燃机这类热电联产机组(CHP)、燃气锅炉、电锅炉、电储能和储热罐,系统同时承担电负荷和热负荷。随机特征主要体现在两个方向:源侧是风电光伏出力的间歇性和预测误差,荷侧是电热负荷的不确定性。处理随机特征最朴素的思路就是场景法——把这些随机变量的概率分布转换成一组典型场景,每个场景带一个概率权重,调度模型的目标函数变成“所有场景下的期望运行成本最低”,约束在各场景下都成立。这样做的好处是模型可以保持线性,用混合整数线性规划求解,速度快、能保证全局最优性,这也是很多论文和工程实践首选场景法的原因。
项目中我选择了并网型微网作为研究对象,也就是系统可以和外部电网交换功率。原因很简单:并网型微网的目标函数能同时反映购电成本、售电收益、燃料成本和运维成本,经济性表达比较完整;而离网型微网还需要加负荷切除约束和频率约束,复杂度更高,不适合作为研究随机优化的第一个案例。整个项目包含四个模块:源荷随机场景生成、CHP系统建模、经济调度目标与约束、Matlab代码实现与仿真分析。下面我按实际开发和调参的顺序,把这几个模块逐步拆开讲。
2. 源荷随机场景生成:从概率分布到典型场景
2.1 随机变量的概率分布怎么选
随机特征不能凭空给,得有概率模型支撑。风力出力的不确定性来自风速波动,一定时间尺度下的风速通常用两参数Weibull分布描述,形状参数k一般取2附近,尺度参数c根据平均风速换算,平均风速6~8m/s、k取2.0~2.3是常见取值;光伏出力与光照强度相关,光照强度在晴空模型基础上叠加扰动,可以用Beta分布描述;电负荷和热负荷则用预测值加正态分布误差的方式模拟,标准差一般取预测值的5%~15%,这个范围覆盖了大多数短期预测误差水平,取值越保守越能体现随机特征。
我做场景生成时用的是蒙特卡洛抽样,每种随机变量按各自的分布独立抽样,然后组合成一个场景。比如抽样次数N=1000,每次抽样得到一组风速、光照强度、电负荷和热负荷的24小时序列,这组序列代入风速-功率转换关系得到风电出力,代入光辐照-功率关系得到光伏出力,这样就形成了1000个完整的“源-荷”场景。这里有个容易踩的坑:风速到风电出力的转换函数必须分段建模,切入风速、额定风速、切出风速三个断点,避免在低风速段出现功率突变,否则场景数据不干净,后面优化结果也会离谱。
2.2 场景削减为什么必须做:K-means与同步回代削减
1000个场景直接扔进优化模型,决策变量和约束数量会爆炸,MIP求解耗时会从几秒飙升到几小时,并不实用。所以场景削减是随机优化里必须走的一步,削减的本质是“用尽量少的代表性场景,尽量逼近原始场景集的概率分布特征”。
我在项目里对比过两种主流削减方法。第一是K-means聚类:把每个场景看成高维空间里的一个点,通过聚类把1000个点归并成10~20个类,用每个类的质心代表原始场景,类的样本数占比作为该场景的概率。算法简单,Matlab自带的kmeans函数可以直接用,聚类数K的选择可以通过轮廓系数辅助判断,项目里取10~20效果都不错。第二是同步回代消除法(backward reduction):每次迭代找出一个与原场景集概率距离最小的场景并删掉,同时把它的概率累加到距离最近的场景上,重复删除直到达到目标场景数。这个方法在学术论文里常用,但需要自己写递归过程,复杂度高一些。
实际调参中我发现,K-means更适合这个场景因为简单可控,尤其是对包含风电、光伏、电、热四个维度的场景集,聚类能在削减的同时自动把“形状相似”的场景合并,而同步回代法更容易受初始场景分布影响,极端场景保留率不稳定。削减后要注意所有场景概率之和必须等于1,不然目标函数里期望成本的计算就有偏差。
2.3 场景削减后的概率归一化处理
每个典型场景的概率不是随便给的。用K-means时,统计每类样本数占总样本数的比例,这个比例就是场景权重。用同步回代法时,初始每个场景概率相等,删除过程中不断累加概率,结束后得到一组带权重的场景。两类方法算出来的权重都要输出校验:概率求和是否为1,是否所有概率大于0。
我处理权重时还习惯画一个削减前后的对比图,把削减后场景的风电出力曲线叠加在原始1000条曲线背景上,肉眼检查削减后的场景是否还能覆盖原始数据的波动范围。这里会暴露一个典型问题——如果K值取太小,比如只取3,削减后场景可能全部落在平均出力附近,极端高风电和极端低风电场景全丢了,优化结果就会偏乐观,调度方案在极端天气下可能扛不住。所以我建议K值下界取10,同时把削减后场景集中最极端场景的概率和原始数据中出现极端场景的概率做对比,控制在合理范围内。
3. 热电联供微网优化模型构建
3.1 目标函数:怎么把随机期望融入运行成本
目标函数是优化的核心。这个项目中我定义的是“所有场景下系统日运行总成本的最小化期望值”,包含燃料成本、购售电成本、运行维护成本,以及弃风弃光惩罚项。
燃料成本主要来自CHP机组和燃气锅炉消耗的天然气。CHP机组的耗气量是电出力的二次函数,近似成系数乘以电功率加常数也可以接受,如果追求更高精度就做分段线性化;燃气锅炉就是单位热出力耗气量乘热功率。购售电成本方面,购电按分时电价加实时功率,售电按上网电价,整个0-1变量控制不能同时购电和售电。运维成本按各设备的单位维护成本乘出力累加。
惩罚项一定不能省。如果没有弃风弃光惩罚,求解器在给定燃气机组成本高于风光出力成本时,倾向于把风电光伏砍到零,系统看起来“很经济”,实际上完全没有利用可再生资源,这不是我们想看到的调度结果。项目里弃风弃光惩罚成本取一个略高于购电成本的值,让求解器在“弃风光去省燃气费用”和“用风光但要付出一点惩罚代价”之间做权衡,这样仿真结果更贴近实际。
从期望表达式写起,目标函数写成:
[ \min \sum_{s \in S} \pi_s \left( C_{gas,s} + C_{grid,s} + C_{om,s} + C_{curt,s} \right) ]
其中π_s是场景s的概率权重,S是削减后的典型场景集合。这个公式在代码里用Yalmip表示时,把所有场景下的成本变量按概率加权求和即可。注意所有场景共享相同的决策变量结构,只是随机参数不同,所以模型规模是场景数乘以单场景变量数,这也是为什么削减场景数不能太大的原因。
3.2 约束条件梳理:电热平衡、设备出力与储能约束
约束条件是整个模型的骨架,直接影响模型的可行性和求解效率。电功率平衡约束:每个场景每个时段,风电、光伏、CHP电出力、电储能放电、购电之和减去售电,等于电负荷加上电锅炉耗电、电储能充电。写成Yalmip约束时,这里的等式在求解器中就是一组线性等式。
热功率平衡约束:CHP机组回收热功率、燃气锅炉热功率、电锅炉热功率、储热罐放热功率之和,等于热负荷加上储热罐充热功率。CHP机组的热电耦合关系是热电联供系统区别于普通电力系统的关键,燃气轮机的产热和产电不是独立的,热电比要么固定,要么在一定范围内可调。项目里用的模型是:热功率等于电功率乘热电比,热电比允许在一个区间内连续调节,但调节范围不能超出机组设计限制。
设备出力上下限约束和爬坡约束没什么特别,每个设备都有最小技术出力、最大出力、单位时间爬坡限制。储能约束稍微复杂一点:SOC状态转移方程 \( SOC(t+1) = SOC(t) + \eta_c P_c(t) \Delta t - (P_d(t)/\eta_d) \Delta t\),充放电功率上限、SOC上下限,还有一个重要约束“同一时刻不能同时充放电”。这个约束如果不加,求解器可能为了“洗钱”同时充放电做无意义的能量循环,模型结果虚高或虚低,加了0-1变量后自然规避。
我在项目模型里还加入了系统备用容量约束,每个场景下系统可用功率必须大于负荷加备用需求。这个约束在随机优化里挺容易被忽略,但恰恰是体现“考虑随机特征”价值的地方——预留备用才能保证极端场景下不切负荷。
3.3 模型线性化与求解器配置思路
模型里包含0-1变量(机组启停、储能充放电状态、购售电状态),以及CHP机组耗气量二次函数,所以严格来说是一个混合整数非线性规划(MINLP)。直接用非线性求解器,求解慢且容易陷入局部最优。我的处理方式比较务实:把二次燃料成本做分段线性化,在Yalmip里面可以用内置的implies和binvar组合实现,分段数取3~4段精度就够了,线性化误差控制在百分之几以内,对调度方案影响很小。
为什么非要做线性化?因为线性化之后模型就是混合整数线性规划(MILP),主流的商用求解器对MILP的支持非常成熟,分支定界配合启发式算法,能在可接受时间内找到全局最优解或近似最优解。非线性模型即使能用求解器跑,每次求解时间也不可控,这在做随机优化的研究中非常致命——因为场景数多,等于要同时在几十上百个“平行时空”里求一个统一调度策略,模型规模大,非线性迭代很容易直接卡死。
求解器配置方面,项目里我用的Yalmip环境可以统一调用多个求解方案,实际求解时通过ops = sdpsettings('solver','xxx')指定,'xxx'这个名字可以替换成机器上已安装的任意MILP求解器。求解前我习惯设置一个合理的MIP gap,比如1%,别让求解器为了证明最优性花大量额外时间。这一步虽然不起眼,实操中能把求解时间从十几分钟降到两分钟,效果非常明显。
4. 基于Matlab的代码实现与求解流程
4.1 代码整体结构与主程序框架
项目代码没有用复杂的面向对象结构,全程脚本加函数,保持了“可读性优先、数据流清晰”的风格,这样也方便后续扩场景数和改目标函数。主程序文件名就叫main_chp_microgrid.m,全局流程是:先初始化参数,再生成并削减场景,然后构建优化模型,求解,最后绘图。
init_params.m:所有基础参数集中在这个函数里,包括负荷曲线、分时电价、设备容量、效率、成本系数、场景抽样相关参数。集中管理的最大好处是调参不用满文件找,改一处就能对比不同工况。scenario_gen.m:蒙特卡洛抽样生成原始场景。scenario_reduction.m:用K-means把原始场景削减成典型场景并返回概率权重。build_model.m:输入典型场景和参数,输出Yalmip模型的目标函数与约束集合。plot_results.m:输出结果曲线和表格,包括各场景下的机组出力和SOC曲线。
每个模块各自独立,后期如果想换成鲁棒优化模型,只需要改build_model.m一个文件即可。
4.2 Yalmip建模的关键代码逻辑
Yalmip建模看起来简单,但这个项目里变量多、维度多、约束多,如果变量定义不规范,后期约束容易出现维度不匹配。我定义变量的思路是“按设备+场景+时段建三维或二维矩阵”。比如CHP机组电出力定义为sdpvar(24, N_scene),第t行第s列代表场景s下第t小时的电出力;储能SOC定义为sdpvar(25, N_scene),因为初始SOC也算一个点,这样递推约束写起来方便。
设备启停0-1变量、储能充放电状态变量都定义为binvar(24, N_scene)。约束的写法上有个小技巧:能用矩阵运算一次性赋约束的地方,尽量写成完整矩阵表达式,少用双层循环写标量约束。比如功率平衡约束写成所有设备出力矩阵相加等于负荷矩阵的广播形式,代码短,模型构建也快。如果用双层for循环逐个时段写约束,场景多的时候Matlab的符号运算会明显变慢,这是我在版本迭代中实际感受到的差异。
还有一个需要特别注意的细节——购电和售电的互斥约束。电网交互功率不是单变量正负,而是用购电变量P_buy和售电变量P_sell两个非负变量表示,再加上一个0-1状态变量,约束该时段P_buy和P_sell不能同时大于0。这个处理在代码里看起来多写了几行,但结果表达非常清晰,而且便于在结果统计时分别核算购电成本与售电收益。
4.3 求解调用、结果回收与单位一致性
调用求解器的代码大概是这样:
ops = sdpsettings('solver','xxx', 'verbose', 2, 'mip.tolerances.mipgap', 0.01); optimize(Constraints, Objective, ops);这里xxx占位符的意思是使用已安装的MILP商业求解器,具体名称根据本机环境替换即可。跑完后立即检查optimize返回的info.problem是否为0,如果是其他值,就要看info.solveTime和info.solveroutput里的信息排查问题。这一步我在早期版本经常忽略,结果求解器已经提示不可行或数值问题,我还在分析一组无意义的解。
单位一致性是另一个容易翻车的地方。项目里功率用MW、能量用MWh、价格用元/MWh,时间步长取1小时,SOC递推时直接乘以1小时,不会有换算错误。我看到不少人代码里一会儿用kW、一会儿用MW,储能容量用kWh,结果SOC递推完全错位。总之所有物理量在进入模型之前必须统一到一个单位体系,建议全部在init_params.m里统一换算好,不要在约束里乘来乘去。
求解完成后,目标函数值、各设备出力、储能SOC、购售电功率都从优化结果里索引出来,整理成结构体数组保存。每个场景变量对应的数值用value()提取后,再画图时才能正常操作。
5. 仿真结果分析与方案对比
5.1 三种对比方案的设置逻辑
为了体现“考虑随机特征”的价值,我没只跑一个模型就完事,而是设置了三个对比方案。
方案一为确定性模型:风电、光伏、负荷全部取预测均值或典型日数据,目标是单场景下的最低运行成本。这是对照组,也是很多初学优化调度最容易先写的模型。方案二为场景法随机优化模型:用10~20个典型场景,目标为期望总成本最小(也就是项目正式方案)。方案三为极端保守方案:只取所有场景中最恶劣场景作为代表进行优化,对应鲁棒优化的思路,可以理解为随机优化的一个参照上限。
这组对比跑完之后,结果非常有说服力。确定性方案计算出的成本看似最低,但在所有1000个原始场景下一一回放校验时,会有部分场景出现失负荷或备用不足,实际风险成本很高;随机优化方案的单场景期望成本比确定性方案高几个百分点,但1000个场景回放全部通过,且不发生切负荷;极端保守方案运行成本最高,却有点“反应过度”,日常场景下利润损失明显。这正是“考虑随机特征”最直观的价值体现:用一点可接受的期望成本增加,换来调度方案在真实随机环境下更强的鲁棒性。
5.2 关键结果图怎么看:出力求、SOC与电热平衡
仿真结果的呈现我分为三张图。第一张是风电、光伏、CHP电出力、电锅炉耗电、储能充放电和购售电叠加后的电功率平衡曲线图;第二张是热功率平衡图,“CHP产热+燃气锅炉+电锅炉+储热放热”和“热负荷+储热充热”上下堆叠,任何一个时段两侧不闭合,图上一眼就能看到;第三张是储能和储热罐的SOC曲线,重点看SOC是否始终在上下限之间,全程是否有频繁充放切换。
画SOC图之前有个细节必须处理——SOC结果通常是矩阵,每个场景一条曲线,如果10个场景全画在一起会糊成一片。我的做法是选概率权重最大的三个典型场景画成实线,其他场景画成浅色细线作背景,这样既能看到典型工况,又能体现场景间的波动区间,分析起来非常清楚。
5.3 换场景数、换碳价的敏感性分析
随机优化研究如果不做敏感性分析,很难回答“参数取这个值为什么合理”。我在评审和实际写报告时最常被追问的两个问题就是:场景数量对结果影响多大?碳交易价格对设备出力结构影响多大?
针对场景数量的敏感性的做法是:分别取K=5、10、15、20、30做削减并求解,记录目标函数值和求解时间。一般K增大会带来成本曲线先降后平的走势,因为场景数太少时削减误差大,增加场景数能更准确描述不确定性,但超过某个阈值后边际收益递减。求解时间却是接近线性增长的,所以项目里K=15基本是精度和时间的平衡点,这是一个可复用的经验值。
针对碳交易价格的敏感性:在目标函数里引入碳交易模块,给CHP机组和燃气锅炉的碳排放量设置配额,超出配额的部分按碳价购买,未用完的配额可以出售。把碳价从低到高扫描,观察设备出力结果会发现:低碳价时燃气锅炉和CHP满发;碳价提高后,CHP与燃气锅炉出力下降,电锅炉和储热罐充放策略变得活跃,弃风率下降。这个结果可以直接作为“碳价引导电热耦合设备低碳转型”结论的依据,也是让整个项目从纯经济调度提升到“经济-低碳联合调度”的关键点。
6. 常见问题与排查技巧实录
6.1 求解器报错与不可行解的排查
做随机优化碰到最多的就是求解器报“infeasible problem”。初看以为是真的无解,实际原因通常是约束内部矛盾。我在这个项目里遇到过一次经典情况:给CHP机组设置了最小技术出力30%的约束,又给热电比设置了固定比例,结果电负荷低谷时段,CHP为了满足最小电出力产生的热量远大于热负荷,热功率平衡就冲突了。解法是把“热功率等于电功率乘热电比”的等式约束改成不等式加辅助变量,允许机组在一定情况下轻度弃热,或者加一个散热调节阀。这个改动在物理上非常合理,实际热电联供机组本来就有凝汽器可以调节供热,模型里加入散热量变量后,约束从等号变成了含散热变量的等式,可行性问题迎刃而解。
求解器报“unbounded”通常是因为目标函数里有变量没有任何成本系数,或约束没有下限。比如储能SOC的初始值忘记赋值,却对末值做了限制,变量就可能通过SOC转移方程被无限推高。遇到这个情况,我会把每个变量都检查一遍是否都有合理上下界,再输出一下每个变量的value(),看到Inf的就是突破口。
6.2 求解时间爆炸怎么办
MIP求解时间太长是这个项目的另一个痛点。我实测过,场景数从10涨到30,求解时间可能从30秒涨到20分钟。排查时先看模型规模,再用求解器日志看是哪个环节耗时。实际操作上我用三招解决:
- 第一招是调MIP gap到1%甚至2%,求解器在接近最优时提前终止,目标函数偏差极小,求解时间大幅缩短。
- 第二招是检查是否引入了不必要的整数变量。比如储能充放电状态变量其实可以合并处理,通过两条方向相反的松弛约束加互补约束来实现,变量数量砍半,分支定界效率提升明显。
- 第三招是减少完全冗余的场景时段连续性——热负荷的随机性在时段间本身平滑,可以减少热负荷场景的削减维度,只对电负荷做场景削减。
这几招优化后,项目典型工况求解时间能控制在两分钟以内,对于研究用途完全够用。
6.3 场景生成时的异常数据清洗
蒙特卡洛抽样容易抽到物理上不合理的组合。比如Beta分布模拟光照时,如果形状参数设置不当,可能抽到凌晨出大功率、中午接近零功率的荒唐曲线。我的建议是在scenario_gen.m中加入一个“合理性校验”步骤:遍历所有抽样场景,把风电出力超过风机额定功率、光伏出力超过装机容量、负荷出现负值或超过峰值的场景直接剔除,避免脏数据进入场景削减环节。
这个清洗步骤在初学者代码里几乎见不到,但实践中极其重要。抽样上千个场景总会有几个异常点,这些异常点进入聚类后,会干扰质心位置,拖累削减后场景的整体质量。剔除比例控制在1%以内即可,不影响概率分布的整体形态。
6.4 调试三步法与check函数的使用
最后分享一个我调试这类模型屡试不爽的实操经验。不要一上来就跑全场景随机优化,而是采用“三步走”策略:
- 第一步,把所有场景数设成1,所有随机变量取期望值,把模型退化成确定性MILP。这一步先保证模型本身物理约束正确、目标函数正确。
- 第二步,把场景数设成3,跑小规模随机优化,检查场景权重引入后的目标函数预期增减是否合理。
- 第三步,恢复完整场景数,跑正式版本。绝大多数模型逻辑错误都会在前两步暴露出来,避免在完整规模上浪费大量排查时间。
Yalmip里有个很好用的checkset(Constraints)函数,可以逐条检查约束残差。我在第二步必用它,把不可行或未满足的约束用残差排序打出来,基本可以定位到具体的约束行。这个工具比手工翻代码快一个数量级,强烈推荐。
7. 做完这个项目后的一些实际体会
整个项目从搭确定性模型起步,到加入场景生成、削减与随机期望目标,再到加碳交易与敏感性分析,代码量和思维量都是逐步累加的。如果让我给后来者一个最直接的建议,那就是把“确定性模型”做扎实再做随机化,不要一上来就铺开大场景集。单位统一、约束物理一致性、决策变量边界完整这三件事,任何一处没做好,后面全场景模型的调试都会痛不欲生。
还有一个容易被低估的细节是场景削减质量的可视化。花十几分钟把削减前后的场景曲线叠加画出来,你会发现很多建模时根本想不到的问题,比如极端场景丢失、概率分布偏移、削减后曲线波动范围变窄。这些数据质量问题,单看数字指标很难发现,图一出来就清清楚楚。
如果你后续想把项目延伸,方向其实很顺:一是在源荷不确定性中加入相关性建模,比如同一地区风资源和光照本身就存在负相关,用Copula函数处理相关性;二是把目标函数从单目标经济性改成经济-碳排双目标,再做Pareto前沿分析;三是把调度从日前单阶段扩展到“日前+日内滚动”两阶段,前段用场景法做计划,后段用模型预测控制做反馈修正,这样整个系统对随机特征的处理会更完整。这几个方向我都试了一部分,代码结构在现在的模块化框架下扩展起来比较顺畅,这也是我当初强调“脚本加函数、模块清晰”的真正原因。