Matlab+YALMIP+Gurobi实现热电联产选址定容优化建模
2026/9/19 2:07:54 网站建设 项目流程

做综合能源系统规划的朋友,对热电联产(CHP)选址定容这个问题应该不陌生。这两年“双碳”目标下,园区级、县域级的综合能源项目扎堆上马,核心设备就是燃气轮机、内燃机这类热电联产机组。可机组装在哪、装多大,直接决定了整个系统的经济性和能效水平,这一步没想清楚,后面管网投资、运行调度全是坑。我去年在做一个北方工业园区的能源规划项目时,正好把这块用Matlab完整做了一遍建模和求解,今天把思路、代码框架和踩过的坑整理出来。这篇文章适合正在做综合能源系统规划课题的研究生、设计院的工程师,以及刚接触选址定容问题、想用Matlab快速上手跑通流程的朋友。我会从问题建模思路讲起,再落到代码实现细节,最后把调试中遇到的典型问题一并列出来,尽量让你少走弯路。

1. 热电联产选址定容到底在解决什么问题

1.1 为什么选址和定容必须一起做

很多刚接触这个方向的人会问:选址和定容能不能拆开做?比如先根据热负荷分布确定机组位置,再算容量。理论上可以,工程上问题很大,因为这两个变量是强耦合的。机组装在哪,决定了它到各热用户的管网距离和热损耗,热损耗又直接影响需要多大容量才能满足末端需求;反过来,容量选多大,决定了占地面积、燃料供应、电网接入条件,这些反过来约束了哪些位置是可选的。

举个最简单的例子:供热半径越大,热网投资越高,热损耗也越大。如果只优化容量不考虑位置,计算结果往往是“选一个离负荷中心很近的地方”,但你不可能在所有负荷中心都建机组——地价、环保、安全间距都有限制。所以选址定容本质上是一个同时决策“位置”和“大小”的组合优化问题,必须放到一个模型里联立求解。

另外,热电联产机组和纯凝发电机组还有一个本质差别——它同时输出电和热,而热负荷和电负荷的时间曲线往往不一致。这就意味着选址定容不仅是一个空间优化问题,还要考虑时间维度上的运行策略。你在模型里怎么简化这个时间维度,直接决定了求解规模和你能不能算出结果。

1.2 问题本质:一个多约束优化模型

从数学角度看,选址定容问题可以抽象为这样一个混合整数规划:

  • 决策变量:候选站址的0-1选择变量、每个站址的装机容量(连续或离散)、典型日的运行工况(出力水平)。
  • 目标函数:年化总成本最小,包括机组投资成本、运行维护成本、燃料成本、热网投资成本,有时候还要加上电网购电成本和碳排放惩罚。
  • 约束条件:电力平衡、热力平衡、机组出力上下限、爬坡约束、热网传输能力约束、站址数量限制、单点容量上限等。

这里面最核心的难点是0-1变量带来的组合爆炸。候选站址如果有20个,理论上就有2的20次方种组合,再加上连续变量和约束条件,直接穷举是不现实的。所以这类问题通常用混合整数线性规划求解器(如Gurobi、CPLEX)或者启发式算法(如粒子群、遗传算法)来处理。

我在项目中选的是“Matlab + YALMIP + Gurobi”这套组合。YALMIP是一个建模工具,能把优化问题用接近自然语言的方式写出来,然后调用底层求解器去解。Gurobi是目前公认的混合整数线性规划最强求解器之一,处理这种选址问题速度很快。这套方案的好处是——代码可读性强、调试方便、求解性能有保障。

1.3 关键输入数据与获取方式

模型能不能算出靠谱的结果,很大程度上取决于输入数据的质量。做选址定容至少需要四类数据:

第一类是空间数据,包括候选站址的坐标、可用面积、地价、与电网接入点的距离、与天然气管网的距离。这些数据一般从园区规划图纸和实地踏勘获得。第二类是负荷数据,包括全年8760小时的电力负荷和热力负荷曲线,至少要有典型日和典型季的数据。第三类是设备参数,包括不同类型CHP机组的热电比、电效率、热效率、单位投资成本、维护成本、寿命周期。第四类是能源价格参数,包括天然气价格、上网电价、购电电价。

这里面最容易出问题的是热负荷数据。很多项目拿不到实测的逐时热负荷数据,只能靠建筑面积和热指标估算,误差很大。我的建议是——如果条件允许,尽量对比两个以上渠道的数据(比如设计院的估算值和同类园区的实测值),取一个偏保守的方案。数据质量差的情况下,再精细的优化模型也只是在错误的地基上盖楼。

2. Matlab建模求解的整体思路

2.1 为什么选Matlab而不是Python或其他工具

我知道现在很多人推荐用Python做优化建模,比如PuLP、Pyomo这些库,功能确实也强大。但对我来说,Matlab在综合能源领域仍然有不可替代的优势。首先是矩阵运算的天然亲和性,能源系统的潮流计算、热网水力计算很多都依赖矩阵操作,Matlab写起来最顺手。其次是Simulink和各类工具箱的配合,与Simscape Electrical、Simscape Fluids这些物理域建模工具的交互非常顺畅,便于后续做仿真验证。

第三点也很现实——高校课题组和设计院里大量已有的能源系统分析代码都是Matlab写的,用Matlab意味着可以直接复用和对比前人的研究成果。如果你所在团队已经有一套完整的负荷预测或潮流计算代码,用Matlab做选址定容能够无缝衔接,省去跨语言转换的巨大工作量。

当然,Python的优势在于生态丰富、开源免费,如果你做的是纯算法研究且在团队内是从零起步,Python也完全可行。我的经验是,工具选型一定要跟着团队经验和项目需求走,别为了“用新不用旧”给自己增加不必要的迁移成本。

2.2 整体框架与模块划分

在动手写代码之前,我习惯先画一张功能模块图(不涉及具体实现,只是思路指引),把整个程序拆成五个独立的部分:数据预处理模块、模型参数设置模块、决策变量与约束建模模块、求解器调用模块、结果输出与可视化模块。

数据预处理模块负责读入原始负荷数据、空间数据、设备参数,做单位统一、归一化和典型日聚合。模型参数设置模块把散落在各个Excel表格里的数据整理成结构体(struct),统一传给优化模型。决策变量与约束建模模块是核心,用YALMIP语言定义变量、目标函数和约束条件。求解器调用模块设置求解器参数(如gap值、时间限制),执行求解并捕捉求解状态。结果输出与可视化模块把优化得到的选址方案、容量配置、运行工况输出成表格和图表,便于后续报告撰写。

这样模块化设计的好处非常明显——调试时可以逐模块验证,数据变了只改预处理部分,约束条件变了只改建模模块,不会牵一发而动全身。我第一次写的时候把所有逻辑堆在一个脚本里,一个变量名写错就要从头查起,后来狠下心重构,效率提升了一个量级。

2.3 求解器选型:YALMIP + Gurobi 还是智能算法

这是很多初学者最纠结的问题。先说结论:如果模型能写成线性或混合整数线性形式,优先用数学规划求解器,也就是Gurobi、CPLEX这一类的商业求解器,或者Matlab自带的intlinprog。数学规划求解器有全局最优性保证,求解速度快,在工程实践中的接受度也高。

那智能算法(粒子群、遗传算法)还有用吗?有,但适合的场景更窄。比如目标函数和约束条件高度非线性、不可导,或者决策变量空间太复杂导致数学规划建模困难时,启发式算法可以作为一种兜底方案。我自己的建议是——不要一上来就赶时髦用智能算法,先试着把你的模型线性化,90%的选址定容问题都能表达成混合整数线性规划。

YALMIP相对于直接用intlinprog的优势在于建模语言更灵活。比如要定义一个0-1变量,直接用binvar;要定义一个中间变量表达“某节点被选中后的建设成本”,用一个辅助变量加一组约束就能线性化。这些在YALMIP里写起来很自然,改模型的时候也不会牵连其他部分。加上它对Gurobi、CPLEX、MOSEK等多种求解器做了统一封装,换求解器就是改一行代码的事,非常方便。

2.4 数据预处理与典型日选取

数据预处理这个环节看起来不起眼,但往往是决定代码能不能跑通的关键。最大的坑在于数据对齐——热负荷的单位可能是GJ/h,电负荷的单位是MW,天然气低位热值的单位和价格单位也常常不一致,如果不统一到一套单位体系里,算出来的结果完全不可信。

第一步是统一单位。我在项目中全部换算成国际单位制下的MW和GJ,燃料消耗用MW为单位的燃料输入功率表示,价格统一为元/MWh或者元/GJ。第二步是处理异常值和缺失值,电网的负荷数据一般比较干净,热负荷数据往往有缺失或毛刺,需要用插值或者同类日替代的方法修补。

第三步是典型日选取。全年8760小时数据直接放进优化模型,变量规模会爆炸,求解时间让人无法接受。常规做法是选取典型日——冬季典型日、夏季典型日、过渡季典型日,或者再用K-means聚类选出更有代表性的几日。每个典型日乘以对应天数作为权重,既能反映季节性差异,又能把模型规模控制在可处理范围内。我的做法是选4个典型日:冬季最大热负荷日、冬季平均热负荷日、夏季最大电负荷日、过渡季平均日,实践证明精度和效率比较平衡。

3. 代码实现与关键模块拆解

3.1 数据初始化:把原始数据塞进可计算的矩阵

我先定义模型的基本参数和数据结构。这里以12个候选站址、3种待选CHP机组类型为例,搭建一个可以直接扩展的骨架。数据初始化这一步的目标是把Excel表格和原始记录转化为Matlab可操作的结构体,同时完成基本的单位换算和数据校验。

%% 数据初始化示例 clear; clc; close all; %% 基础参数设置 n_site = 12; % 候选站址数量 n_type = 3; % 可选CHP机组类型数量 n_day = 4; % 典型日数量 hours = 24; % 每日小时数 %% 候选站址数据:坐标、可用面积、地价、与电网/气网距离 % 每行代表一个站址:x坐标(km),y坐标(km),可用面积(m2),地价(元/m2) site_info = [ 2.3, 5.1, 3200, 1200; 4.7, 3.8, 2800, 1350; 6.1, 7.2, 3600, 1100; 8.4, 4.6, 3000, 980; % ... 共12行,按实际数据填写 ]; %% CHP机组技术参数:电效率、热效率、热电比、单位投资、寿命 % 电效率、热效率、最大容量(MW)、单位投资(万元/MW)、寿命(年) chp_params = [ 0.42, 0.45, 5, 3500, 20; 0.46, 0.42, 10, 3000, 20; 0.40, 0.48, 3, 4200, 20; ]; %% 负荷数据:典型日逐时电负荷和热负荷(单位统一为MW) % load_elec(n_day, hours),load_heat(n_day, hours) % 这里用随机数据示意,实际请从Excel读取 rng(2024); load_elec = 20 + 5 * rand(n_day, hours); load_heat = 15 + 8 * rand(n_day, hours);

这一步看着简单,但有几个关键细节:

第一个细节是坐标和距离矩阵。后面热网投资成本计算需要知道候选站址与热负荷中心的距离,这个千万别用真实地图的直线距离,要考虑路网折减系数。我的做法是先用经纬度转平面坐标(UTM投影),再乘一个1.3左右的管网曲折系数,更贴近实际工程。

第二个细节是负荷数据的量级差异。我遇到过电负荷是几十MW量级,热负荷只有几MW的情况,如果单位不统一,目标函数里热网投资和燃料成本的权重会完全失衡。所以数据初始化模块的最后一步,一定要加一个自动检查逻辑——打印各个数据列的最大值和最小值,肉眼确认量级是否合理,再进建模环节。

3.2 决策变量与目标函数怎么写

用YALMIP定义变量非常直观。选址变量是0-1矩阵,行对应站址、列对应机组类型;容量变量是连续变量;运行工况变量是三维矩阵——站址、机组类型、时段。这里有个建模技巧需要注意:单台机组可以选择“不装”,所以运行工况变量要和选址变量联动。实际使用中我一般用一个辅助变量表示“某站址某类型机组的投建数量”,再把容量和建造成本都挂在这个变量上,避免复杂的变量联动。

%% 决策变量定义 x = binvar(n_site, n_type); % 选址变量:1表示在i站址安装j类型机组 cap = sdpvar(n_site, n_type, 'full'); % 装机容量连续变量(MW) % 运行工况变量:典型日d、时段h、站址i、机组类型j % 为了简化,这里把运行变量降维处理 p_chp = sdpvar(n_day * hours, n_site * n_type, 'full'); q_chp = sdpvar(n_day * hours, n_site * n_type, 'full');

目标函数是年化总成本最小,我拆成了五个部分:机组投资年化成本、运行维护成本、燃料成本、热网投资成本、购电成本。其中投资成本要按寿命期做等年值折算,不能直接把总造价放进目标函数,否则相当于把所有成本都压在第一年,结果偏差很大。

%% 目标函数:年化总成本最小 year_cost = 0; r = 0.08; % 折现率 %% 1) 机组投资年化成本(等年值折算) for i = 1:n_site for j = 1:n_type invest_year(i, j) = chp_params(j, 4) * cap(i, j) ... * (r * (1 + r)^chp_params(j, 5)) / ((1 + r)^chp_params(j, 5) - 1); end end year_cost = year_cost + sum(sum(invest_year)); %% 2) 燃料成本、运维成本和购电成本 % 需要展开到每个典型日和小时后累加,这里示意日均值乘365 %% 3) 热网投资成本:与容量、距离成正比 dist_mat = squareform(pdist(site_info(:, 1:2))); unit_heatnet_cost = 800; % 万元/km/MW 热网单位投资 heatnet_cost = 0; for i = 1:n_site for j = 1:n_site heatnet_cost = heatnet_cost + unit_heatnet_cost * dist_mat(i, j) * cap(i, j); end end year_cost = year_cost + heatnet_cost * (r * (1 + r)^30) / ((1 + r)^30 - 1);

这里有一个我很想强调的建模心得:目标函数里的成本项一定要先明确计量单位,再写表达式。我自己吃过一次亏——燃料成本的单位是万元/年,热网投资折算下来是万元/年,结果忘了把购电成本从元/MWh换算成万元/MWh,最终算出来的最优方案明显不合理,检查了半天才发现是个10的4次方倍的单位问题。

3.3 约束条件建模的核心技巧

约束条件是选址定容模型里最容易出错的部分。我按照“必须满足的硬约束”和“为建模而引入的辅助约束”两个层次来组织,代码逻辑会清晰很多。

首先是电力约束和热力约束。对于每个典型日的每个时段,所有CHP机组的发电功率加上从电网网购的电力,要等于该时段的电负荷;同理,所有机组的热输出要大于等于热负荷(热负荷由热网统一供给,在这里按总量平衡处理)。这里的技巧是——运行变量要和选址变量联动,没有投建的机组,出力必须为零。我用的方法是大M约束(Big-M),M取一个足够大的数(比如机组最大容量的1.5倍),把“x=0时p必须为0”这个逻辑线性化表达出来。

%% 电力/热力平衡约束(示意) M = 50; % 大M值,取足够大 Constraints = []; % 约束1:选中变量与容量变量联动 for i = 1:n_site for j = 1:n_type Constraints = [Constraints, cap(i, j) <= x(i, j) * chp_params(j, 3)]; % 也可以加下限:如果选中,容量不低于最小技术出力对应容量 end end % 约束2:运行出力不超过容量(每个典型日、每个时段) for d = 1:n_day for h = 1:hours for i = 1:n_site for j = 1:n_type idx = (d - 1) * hours + h; Constraints = [Constraints, p_chp(idx, (i - 1) * n_type + j) <= cap(i, j)]; Constraints = [Constraints, q_chp(idx, (i - 1) * n_type + j) <= cap(i, j) * chp_params(j, 2) / chp_params(j, 1)]; end end end end

第二个很容易出错的地方是电平衡约束中的购电变量。实际系统中电网购电是有限制的,不能想买多少买多少,要加一个联络线功率上限约束。另外,CHP机组的“以热定电”和“以电定热”两种运行模式,在约束条件里表达方式完全不同。以热定电模式下,热输出是主变量,电出力等于热出力除以热电比;以电定热模式则反过来。你的模型选择哪种运行策略,决定了变量之间的耦合关系,一定要在约束里写清楚,否则求出来的“最优方案”在工程上根本无法执行。

第三个约束是供热可靠性约束——即使在全年最大热负荷工况下,系统也必须保证达标供热。这意味着至少要有一个典型日能够覆盖峰值热负荷,否则冬季会出现供热缺口。我通常把最大热负荷日单独作为一个典型日放进模型,并附加约束要求该日的热出力冗余度不低于5%。

3.4 求解调用与结果处理

模型建好之后,求解调用反而最简单,但同样有细节。YALMIP的sdpsettings设置里,我一般会关注三个参数:求解器选择、时间限制、MIP gap容差。Gurobi默认的gap容差是很严格的(10的负4次方),对于大规模选址问题,可以适当放宽到1%左右,求解速度能提升好几倍,工程上精度完全够用。

%% 求解设置与调用 ops = sdpsettings('solver', 'gurobi', 'verbose', 2, ... 'gurobi.MIPGap', 0.01, 'gurobi.TimeLimit', 3600); sol = optimize(Constraints, year_cost, ops); %% 结果检查 if sol.problem == 0 disp('优化求解成功'); % 提取选址结果 x_result = round(value(x)); cap_result = value(cap); % 计算总成本明细 total_cost = value(year_cost); else disp('求解失败:'); disp(sol.info); end

求解完成之后,必须做的一步是结果合理性校核。我的标准动作是输出三个东西:每个站址的机组类型和容量、典型日的出力曲线、总成本的构成明细。出力曲线可以直接画出来和负荷曲线对比,看电平衡和热平衡是否满足;成本构成明细能帮你判断结果是否符合直觉——比如热网投资占比过高,那很可能选址分散了,需要调整参数。

还有一个值得提的处理技巧:Gurobi求解MILP问题的结果是“全局最优”或“近似全局最优”,但如果变量规模特别大、gap一直降不下去,你可以用求解器返回的gap值来判断结果可信度。我一般把gap控制到1%以内才认为结果可接受,超过3%的解我会调整参数重新求解。

4. 常见问题与调试经验实录

4.1 求解不收敛或者无解

这是遇到最多的问题。模型一运行,YALMIP直接返回“infeasible problem”,很多人的第一反应是约束写错了,但绝大多数情况下是数据量纲或边界条件冲突导致的。我调试的经验分三步:

第一步检查可行域是否为空。最快的办法是先去掉全部约束,只保留变量定义,跑一遍——如果这都无解,说明YALMIP建模本身就有问题;如果去掉某些约束后有解,再用二分法定位是哪条约束导致矛盾。第二步检查大M值是否合理。M值太大会导致数值病态,求解器精度下降;M值太小又会误伤可行解。我一般把M设置为对应变量的理论上限的1.2到1.5倍。第三步检查是否存在“互斥约束”——比如同时要求热出力大于热负荷,又要求CHP机组在低负荷工况下运行,而机组的技术出力下限不满足,这种约束冲突很隐蔽,要把约束一条条打印出来逐项核对。

还有一种常见的无解原因是候选站址的容量上下限设置太紧。比如某个站址面积只够装1台8MW机组,但你给的容量上限是10MW,加上热负荷约束后可能就无法平衡了。这类问题用松紧诊断法(逐一放宽某条约束看解是否存在)可以快速定位。

4.2 选址结果全部扎堆,或者过于分散

选址结果不合理,是模型本身输出“符合数学约束但不符合工程逻辑”的典型表现。我遇到过两种情况:

第一种是结果把机组全部集中在一个站址。原因通常是热网投资成本在目标函数中的权重太低,或者距离矩阵设置有问题,导致模型没有动力去分散布置机组靠近负荷中心。解决方法是提高热网单位投资成本参数,或者增加一个“单站址最大装机容量”约束限制过度集中。

第二种是结果过于分散,每个站址都装一台小机组。这往往是模型考虑了“机组类型连续可调”的假设,而实际机组容量是离散的,标准产品型号就那么几个规格。解决办法是把容量决策变量定义成离散变量(比如只能选择标准容量档位),或者对机组数量加最小装机约束。

我在项目里最终采用了“候选站址数量上限不超过3个 + 单站址最小装机不低于5MW”的组合约束,结果工程上合理很多。调模型必须带着工程直觉,数学模型不会主动告诉你什么方案在现实中不可行。

4.3 求解时间过长:从8760到典型日的降维

第一次把全年8760小时负荷数据放进模型,Gurobi跑了一个小时还没出结果,我差点以为程序死循环了。后来看了求解日志,发现光连续变量就有几十万个,0-1变量也有上万个,这个规模对MILP求解器来说已经很大了,再加上4小时的时间限制,结果可能都出不来。

解决的方法是降维。我先用K-means聚类算法把全年负荷曲线聚成6到12类,每一类取聚类中心作为“典型日”,再按天数加权。即使只用4个典型日,模型精度也能控制在5%以内,求解时间却从“无响应”缩短到十几分钟。这里有个原则——典型日的选取要以“能反映负荷季节特性和峰值特性”为准,不能只图数量少。我在实际项目中把冬季一个最大热负荷日、冬季一个平均日、夏季最大电负荷日、过渡季平均日这4个典型日作为标配组合,效果很稳定。

更进一步,如果你用的是YALMIP+Gurobi,还可以开启求解器的“保守整数可行解”策略(MIPFocus参数),让求解器优先找可行解而不是死磕最优解,这在规模特别大、只想要一个合理可行方案时非常有用。

4.4 YALMIP与求解器的安装配置坑

最后说一个很多人会卡住的技术细节——YALMIP和Gurobi的安装配置。YALMIP的安装相对简单,把下载的文件夹加入Matlab路径就可以。真正的坑在于Gurobi的许可证配置和Matlab接口。

Gurobi在学术环境有免费许可证,但你需要先注册申请,然后配置环境变量。安装完成后,在Matlab里运行gurobi_setup,如果没有报错,再运行yalmiptest看是否检测到Gurobi求解器。我很多时候遇到的问题是Matlab和Gurobi的版本不兼容——比如Matlab 2023a配新版Gurobi 11,接口报一堆错。稳妥的做法是去Gurobi官网查一下你当前Matlab版本对应的推荐Gurobi版本,严格按版本搭配安装。

如果你申请不到Gurobi许可证也完全不用慌,YALMIP默认调用的Matlab原生intlinprog也能解决大部分规模不太大的问题,只是求解速度慢一些。我在早期测试阶段就是用的intlinprog,模型逻辑跑通后才切换到Gurobi提升性能。先跑通再优化,这个顺序永远不会错。

5. 从代码到项目的延伸思考

这个选址定容代码框架跑通之后,往工程落地走还有几个方向可以扩展。最直接的是把热网模型做得更精细——目前是简化成距离相关的线性投资模型,如果要考虑管网拓扑、管径选型和热损耗的非线性特性,就需要引入管网水力热力耦合计算,模型复杂度会上一个台阶,但结果更贴近实际工程。

另一个方向是加入碳约束和可再生能源渗透率约束。现在很多园区项目对碳排放有硬指标,这需要在目标函数里加入碳成本项或碳配额约束;如果要考虑光伏、风电的接入,还需要在主模型外层嵌套一个容量配置循环,把CHP选址定容和新能源容量优化做联合求解。

我个人实际体会最深的一点是——Matlab代码本身只是工具,真正有价值的是你对问题的理解和建模思路。同样是选址定容,工业园区和居民区的约束条件完全不同,供热半径、峰谷特性、电价机制都不一样。把模型框架搭好、把数据质量控制好、把工程约束理解透,这三点做到了,换什么编程语言都能解出好结果。

最后再分享一个小技巧:每次跑完一个算例,记得用matlab的save命令把结果和参数设置一起存档,文件名加上日期和场景描述。这个习惯我在无数个项目中受益——过两周回来看结果,不会因为忘了当初用的哪组参数而抓狂。希望这套代码框架和调试经验能帮你少踩几个坑,早点跑出满意的选址定容方案。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询