☰
碳交易机制下考虑需求响应的综合能源系统优化运行模型代码解析
2026/10/10 17:01:56 网站建设 项目流程

做综合能源系统优化的人,应该都经历过这样一个阶段:看了不少讲碳交易机制、需求响应(DR)与综合能源系统协同优化的论文,模型图画得很漂亮,但一打开代码就不知道怎么把这些机制真正落进去。这篇文章要讲的,是一套已经跑通的“碳交易机制下考虑需求响应的综合能源系统优化运行模型”代码,我会按功能模块说明它做了什么、为什么这么写,以及调试时踩过的坑。如果你正在做园区级电热冷综合能源系统调度、微电网能量管理,或者准备复现一篇“碳交易+需求响应”方向的论文,这篇内容可以帮你少走不少弯路。

先说结论:这套代码本质上是在求解一个多时段的混合整数线性规划(MILP)问题,优化目标是在满足电、热、冷负荷和碳排放配额要求的前提下,把未来24小时各设备的出力计划、储能充放电计划、负荷调整计划一次算清楚。碳交易机制决定了“排碳要花钱、省碳能赚钱”,需求响应提供了“负荷侧可调度的柔性”,综合能源系统则把这些跨能源品种的设备在数学上串成一个整体。下面我按建模到落地的顺序拆开讲。

1. 这个模型到底在优化什么:一个把“源-网-荷-储-碳”串起来的调度问题

1.1 先从设备清单出发,建立能量流的抽象

我在这套代码里用的是一套典型的园区级综合能源系统配置:屋顶光伏、风机、燃气轮机、燃气锅炉、电储能、电锅炉、热泵,再加上一部分可调节的柔性负荷。模型的目标不是搞清楚某个设备内部的热力学过程,而是把每个设备抽象成“输入能量→输出能量”的转换节点,然后通过电、热、冷三种母线把它们连接起来。

代码里每个设备类都只做三件事:登记输入输出端口、声明运行约束、计算单位运行成本。以燃气轮机为例,它的输入是天然气,输出是“电+热”两种能量,约束包括出力上下限、爬坡速率和热电比运行范围。电储能则更简单,输入输出都是电,但多了SOC(荷电状态)的时序递推约束。我把这套设备关系用一张表固定在数据文件里,模型主体不关心设备具体是什么,只操作一个统一的“端口对象”。

设备输入能量输出能量核心约束
屋顶光伏太阳辐照电出力不超过预测值
风机风速电出力不超过预测值
燃气轮机天然气电+热出力上下限、热电比、爬坡
燃气锅炉天然气热出力上下限、效率
电储能电电SOC递推、充放电功率、容量
电锅炉电热电转热效率
热泵电热/冷能效比、出力上下限
柔性负荷电用电服务可削减/可转移/温控区间

这套抽象方式最重要的好处是:以后想换设备,只需要新写一个类并注册进系统配置,模型主体不用改。比如把燃气轮机换成燃料电池,只要保持输入端口是天然气、输出端口是电和热,约束逻辑在类内部重写就行。

1.2 目标函数不只是“买电最便宜”

这套代码的目标函数写得很直白,但每一项背后都对应一类机制:

最小化总成本 = 外购电费 + 天然气燃料费 + 设备运行维护成本 + 碳排放交易成本 + 需求响应补偿费用 - 余电上网收益。

很多初学的人以为碳交易成本就是“排放量乘一个碳价”,直接写进目标函数里,这么做不完全对。因为碳交易机制下,系统先拿到一个免费配额,如果你的实际排放低于配额,剩余配额可以出售;高于配额才需要购买。所以代码里必须把“购买配额”和“出售配额”分别设成两个非负变量,净支出才等于购买成本减出售收益。

需求响应补偿费用也一样,它代表为了让用户削减负荷或转移用电时段,运营方需要付出的激励成本。这个钱不是模型随意开的空头支票,每花一块钱补偿,都必须换来对应的负荷调整量,否则优化器会发现“疯狂补偿用户、停掉所有机组”是最省钱的方案,结果自然荒谬。

1.3 为什么必须用多时段联合优化,而不是逐时段独立算

如果每个时段单独求解,储能和需求响应这两个核心模块就没法建模。储能在1点时充电,可能为了在5点高峰放电;可转移负荷从下午挪到晚上,也需要全天信息才能判断划不划算。跨多时段的耦合约束让问题变成一个整体。

代码里所有决策变量都带时间索引,例如燃气轮机出力写成 P_chp[t],储能SOC写成 SOC[t],负荷调整量写成 DR_shift[t],然后统一交给求解器做全局优化。我建议调度周期取24小时,时间步长取1小时;如果要做滚动优化,就采用“预测-求解-执行-滚动”的循环结构。时间粒度太细会显著增加变量数量,太粗又无法体现储能的日内转移价值,1小时是平衡点。

2. 代码结构怎么设计,才不辜负这套模型

2.1 按“数据-模型-求解-结果”四层拆,而不是按设备拆

我见过不少论文代码,喜欢按设备建文件:chp.py、storage.py、boiler.py,每个文件把自身的变量、约束、目标项全部写进去。这么做的确和论文结构对应,但一旦涉及碳交易这种跨设备的约束,代码就会变得很难维护。碳排放核算要统计所有燃气设备,需求响应约束要作用于多个负荷端口,如果每个设备文件都“各管一段”,最后拼模型时容易出现约束重复添加或漏加。

我这套代码按功能层拆:

project/ data/ system_config.yaml load_profile.csv renewable_profile.csv price_profile.csv model/ components/ chp_unit.py storage.py electric_boiler.py heat_pump.py flexible_load.py carbon.py demand_response.py operation_model.py solver/ optimize.py sensitivity.py report/ output_summary.py plot_results.py

model/components放单个设备的约束和成本函数;model/carbon.py放碳排放核算边界、配额计算、碳交易成本;model/demand_response.py放各类柔性负荷的约束与补偿计算;operation_model.py负责把设备、碳、需求响应三类约束组装成完整数学模型。这样分层后,任何一个模块改动(比如把阶梯碳价从两档改成三档),其他模块完全不需要动。

2.2 数据层是最容易偷懒,但最应该花时间的地方

模型里的所有边界参数,我都要求集中在data/目录下,代码里不允许出现写死的“魔法数字”。时间序列数据用CSV保存,设备参数和机制参数用YAML保存,每一条都带注释和单位。这样做的好处非常直接:调整碳价曲线、需求响应补偿单价、设备效率时,改完数据文件即可重跑,不用去代码里大海捞针。

我给 carbon 模块的典型配置长这样:

carbon: allowance_method: benchmark # 配额按基准法核算 benchmark_per_mwh: 0.52 # 每MWh系统综合供能对应的免费配额,单位 tCO2/MWh emission_factors: chp_gas: 0.20 # 燃气轮机单位天然气输入对应排放 boiler_gas: 0.20 # 燃气锅炉单位天然气输入对应排放 import_power: 0.58 # 外购电力的间接排放因子 price_blocks: - {upper_bound: 1000, price: 80} # 累计购买配额0~1000吨,单价80元/吨 - {upper_bound: 2000, price: 120} - {upper_bound: 99999, price: 160}

数据校验也应当放在这层。我踩过的最常见的坑是CSV里混入了空行或字符串,求解器界面直接报错但不告诉你错在哪一行。后来我在读取数据后加了一段断言函数,检查每一列是否可转成浮点数、类型是否正确、数值是否在合理区间,早发现问题远比事后排查省时间。

2.3 模型主类的职责边界

OperationModel是这个项目的核心类,但它并不自己读数据、不自己画图,而是只做四件事:声明优化变量、添加目标函数、添加约束集合、调用求解器。我把每个约束都封装成一个带名字的方法,例如add_energy_balance_constraints()、add_carbon_constraints()、add_demand_response_constraints(),这样从日志里看到“约束添加总数”,能快速定位是哪一类约束出了问题。

ResultReport类则负责把求解结果转成可读的形式:设备出力曲线、碳交易量、需求响应情况、逐时成本拆解。模型层和结果层分离后,哪怕换一种求解器,报告模块完全不用重写。

3. 碳交易机制落成代码:配额、交易量、阶梯价格,一个都不能少

3.1 先划分碳排放核算边界

写碳模块之前,必须先回答一个问题:哪些排放算进这个系统的碳排放总量?我在这套代码里把核算边界定义为“物理边界内所有燃料燃烧的直接排放,加上外购电力对应的间接排放”。燃气轮机、燃气锅炉的直接排放要算,电锅炉和热泵消耗的外购电则通过外购电力排放因子计入,不能重复计算,也不能漏算。

代码里用一个排放源字典来登记所有排放源:

EMISSION_SOURCES = { "chp": {"type": "direct", "input_fuel": "gas", "factor": 0.20}, "boiler": {"type": "direct", "input_fuel": "gas", "factor": 0.20}, "import_power": {"type": "indirect", "factor": 0.58}, }

碳排放总量的表达式就是每个排放源逐时段累计求和。注意,这里的单位统一用吨(tCO2),不要和功率单位MW混在一起。时间步长如果不是1小时,要把功率乘以时长折算成能量,否则求和结果会差一个数量级。

3.2 配额分配与交易量变量

配额怎么分,直接影响碳交易成本。这套代码里我采用的是基准法:系统每供出一单位综合能量,获得一个免费配额,配额总量等于“供能量乘基准排放强度”。如果你做年度项目,配额按年给;做日前调度这种短期优化,就把年配额按运行日等比例折算成“日配额 allowance”。

配额交易的核心约束写法是:

[ E_{total} - allowance = purchase - surplus ]

其中purchase和surplus是两个非负变量,分别代表购买配额量和出售配额量。当系统实际排放低于免费配额时,等式右边为负,surplus 承担那个差值;当排放高于配额时,purchase 承担正差值。这个约束看起来简单,但特别容易写反。我建议写完先用简单数字验算:allowance=100,E_total=90,等式要得到 -10=0-10,也就是 surplus=10。

3.3 阶梯碳价的分段线性化技巧

不少地方的碳市场都采用阶梯价格:配额缺口越大,边际购买价格越高。比如缺口在1000吨以内时单价80元/吨,超过1000吨但不足2000吨的部分单价120元/吨。这个分段函数直接写进目标函数会令其变成非线性,我的做法是拆分购买变量再加价格递增约束。

x_buy = x1 + x2 + x3 0 <= x1 <= 1000 0 <= x2 <= 1000 0 <= x3 <= max_extra cost_carbon_buy = 80 * x1 + 120 * x2 + 160 * x3

这里有个关键前提:阶梯价格必须严格递增。因为递增,优化器会优先把低价段 x1 买满,然后才会碰 x2,不需要额外添加0-1整数变量。如果哪天真遇到价格递减的分段结构,就必须引入二进制变量强制分段顺序,否则求解器会把购买量全部塞进低价段,结果完全错误。

4. 需求响应的代码建模:什么样的“柔性”能变成约束

4.1 价格型需求响应和激励型需求响应要分开放

需求响应在工程上通常分两类:价格型(PDR)和激励型(IDR)。价格型通过分时电价引导用户自发调整用电,激励型则是调度方向用户支付补偿、换取确定的负荷削减或转移。在我的代码里,这两类的落点完全不同。

价格型需求响应发生在优化之前:把基础负荷曲线、电价和价格弹性系数输入到一个预处理函数,得到“修正后的负荷预测”,然后当作固定参数传给优化模型。它不是一个决策变量。激励型需求响应则发生在优化模型内部:负荷调整量是决策变量,同时目标函数里增加对应的补偿费用。如果把两类混在一个模块里,很容易出现同一份负荷既被“价格修正”又拿“激励补偿”,财务上重复计算。

4.2 可转移负荷:关键词是“电量守恒+时间窗”

工业用户的可转移负荷,典型特征是全天总用电量固定,但允许把一部分电量从高峰时段挪到低谷时段。我在代码里用两组变量表示:转入电量gamma_in[t]和转出电量gamma_out[t],让每个时段的净调整量等于两者之差。

约束有三条。第一,在允许转移的时间窗外,变量强制置零;第二,每个时段的转移量不超过上限;第三,全天的转入电量总和等于转出电量总和,保证负荷总量不凭空增加或减少。第三个约束特别容易漏,漏掉后优化器会把负荷全部挪到最便宜时段,让“可转移”变成“可消失”。

# 电量守恒:无论怎么转移,全天总用电量不变 sum(gamma_in[t] * dt for t in T) == sum(gamma_out[t] * dt for t in T) # 转移窗口限制:只在允许的时段内产生变量 for t in T: if t not in shift_window: gamma_in[t].UB = 0 gamma_out[t].UB = 0

4.3 温控负荷的柔性区间近似

空调、冰蓄冷这类温控负荷,有天然的热惯性。精确建模要引入室内温度动态方程,但放在一个包含碳交易、储能的综合优化模型里会显著增加复杂度。我的折中方案是线性化近似:把柔性负荷的功率限制在上下限之间,再增加一个“整个调度周期内累计供能量不低于需求下限”的约束。

[ P_{flex,\min} \le P_{flex}[t] \le P_{flex,\max} ] [ \sum_t P_{flex}[t]\cdot \Delta t \ge Q_{ref} - Q_{allow} ]

Q_ref是参考情况下的累计供能需求,Q_allow是允许的偏差裕度。这个近似虽然没有逐时温度精度,但在求解稳定性和工程可接受度之间取得了平衡。要注意再补一个功率变化率约束,否则优化器可能让空调在相邻时段之间剧烈跳变,实际设备根本执行不了。

4.4 补偿费用的计算口径

给需求响应付费时,支付口径要非常小心。对可削减负荷,补偿费 = 单位补偿系数 × 实际削减量;对可转移负荷,不要对全部转入转出量都付费,因为用户只是改变了用电时段,总量没变,成本增加主要来自“用电时间变更带来的不便利”。

我在代码里使用基准线法:补偿金额基于“实际负荷相对基准负荷的减少量”计算,只有削峰方向上的响应才支付补偿。这样写一方面符合工程惯例,另一方面避免优化器利用转入和转出的差值“做账”,在不需要用户配合的情况下套取补偿。

5. 求解层与结果校验:模型跑通最优解,不代表结果能用

5.1 为什么我把模型里的非线性环节全部线性化

这套模型最终交给求解器的是一个MILP问题,而不是非线性问题。原因很简单:MILP有相对成熟的全局求解算法,商用求解器能在合理时间内给出带最优性边界的解;而带有双线性项或非线性函数的模型,求解器常常只能给局部最优,结果还不稳定。碳交易的阶梯价格用分段线性化处理,设备的部分负荷曲线也可以用分段线性近似,电力平衡约束本身就是线性等式,储能递推也只需保持线性。

求解阶段我会设置几个关键参数:MIP Gap控制在0.01%以内,时间上限给到1800秒,允许求解器多线程并行。如果模型规模偏大,优先检查是不是不小心把某类约束加成了双倍;扩大时间上限只是治标。

5.2 典型日的成本拆解示例

我拿一套典型配置的模拟园区数据跑出来的结果长这样:

成本项数值(万元/天)占比
外购电费18.539.4%
天然气燃料费20.243.0%
设备运维费4.59.6%
碳交易成本3.26.8%
需求响应补偿2.14.5%
余电上网收益-1.5-3.2%
总成本47.0100%

这张表最有价值的地方在于,碳交易成本不是一个小零头,它已经能影响调度决策。如果把碳价从80元/吨抬高到160元/吨,总成本大约上升3.7%,但燃气轮机的出力会下降约6%,外购电上升约2%。这意味着这套模型能用来做碳价敏感性分析,判断不同碳价水平下设备运行策略会不会发生切换。

5.3 三个常用诊断指标:影子价格、约束松弛、边际成本

求解模型后,不要只盯着目标函数值,我建议从结果里导出三类信息。第一,各时段功率平衡约束的影子价格(对偶变量),它告诉你哪个时段电或热最紧张。第二,约束松弛量,如果某个约束的松弛显著为正,说明当时条件不紧绷,对应的设备增加空间有限。第三,设备的边际成本,也就是多增加一单位设备容量能省下多少钱,它可以直接指导后续容量配置。

代码里我会让ResultReport自动输出一张“瓶颈时段表”,把影子价格最高的前5个时段列出来。这样写周报或论文分析时不用从海量CSV里自己找,直接引用这张表就行。

6. 我在调试这套代码时踩过的五个坑,以及怎么定位的

6.1 热功率单位和电量单位混用

最初版本里,燃气轮机的热出力我用MW直接和热负荷的MWh相加,等于把瞬时功率和累计能量混在一起。1小时时段内两者的数值刚好一样,所以我没第一时间发现;后来把时间步长改到15分钟,结果立刻偏到离谱。定位方法很笨但有效:我在每个约束的注释里写明“本约束所有项统一使用kW,时间步长h”,并加了一个单位自检函数。现在代码在构建模型后会自动检查每个能量平衡约束的单位标识,不一致直接报错。

6.2 把储能SOC的日循环约束写成了逐时段等量

我曾经为了让储能“一天结束回到初始电量”,错误地写成 SOC[t] == SOC[t-1] 对每个时段都成立,结果运维压根不敢充放电,所有时段SOC被锁死。正确写法先让SOC按物理规律递推,即考虑充放电效率和容量损耗,然后在一天开始和结束之间只加首尾等式 SOC[0] == SOC[T]。中间时段允许正常波动,日循环是“终点等于起点”,不是“相邻时刻相等”。

6.3 阶梯碳价分段函数忘了给最高档上界

最高一档价格如果只写x3 >= 0而不设上界,求解器确实不会出什么数值错误,但预求解阶段会因为变量边界过宽导致数值精度下降。后来我给x_buy加了一个系统最大可能购买量的上限,这个上限由所有排放源理论最大排放之和推得。上界不是随便拍的数字,太紧会切掉可行解,太松又失去数值稳定的意义,取理论上限的1.05倍刚好。

6.4 可转移负荷的绝对值建模把负荷“转没了”

初版代码里我试图用P_abs[t] = |P_actual[t] - P_base[t]|表示调整量,结果模型不仅给增加负荷也算了补偿,还出现用户负荷被“转移”到不存在的时段。后来我彻底放弃绝对值写法,改成转入、转出两个独立非负变量,并且把补偿只挂在削减方向上。绝对值在线性优化里永远应该被拆开,不要指望求解器帮你处理这种“看起来很简单”的数学表达。

6.5 Big-M参数取值太大导致数值病态

在给需求响应增加响应状态变量时,我用过 M=1e6 做线性化,结果求解器频频报数值警告,甚至直接崩掉。问题不在于“该不该用大M法”,而在于M取太大。最优实践是把M设置成相关变量物理边界的1.05倍,比如某台设备最大出力2000kW,M就取2100,而不是随手写个一百万。把所有约束的量级统一到kW和kWh之后,求解稳定性明显改善。

最后再分享一个小经验。上面这些坑,几乎都有一个共同来源:建模时“变量量纲”和“时段粒度的换算”没有在一开始就固定下来。我后来在代码里加了一个参数一致性检查模块,每个设备类的构造函数里都会登记输入输出单位,OperationModel构建完约束之后自动跑一遍单位自检。这样绝大部分低级错误在求解之前就会被拦住,省下来的时间足够再做两轮碳价敏感性和负荷响应比例分析。希望这套“数据-模型-求解-结果”的代码组织方式,能帮你把论文里的模型变成一台真正稳定运行的优化引擎。

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

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

立即咨询