首页 / 资讯中心 / 文章详情

综合能源系统优化:需求响应与碳交易机制的MILP建模实践

综合能源系统优化:需求响应与碳交易机制的MILP建模实践 ★ FEATURED ARTICLE
1. 项目概述一个能源系统的三角难题这两年做综合能源系统优化最深的感受是传统的源随荷动模式正在变成源荷互动 碳约束的多目标博弈。我手头正在迭代的一个园区级综合能源系统项目核心诉求已经不再是单纯的电费节省而是要在满足负荷需求的前提下同时响应电网调度指令、参与碳配额履约还要保证经济性最优。这个项目让我花了大量时间梳理需求响应Demand Response, DR和碳交易机制到底如何从代码层面改变能源供应的逻辑今天就把这些实践经验拆开聊一聊。简单说这个项目的目标是构建一套园区综合能源系统优化调度模型将光伏、储能、燃气轮机、电网购电、冷热负荷等多类能源设备统一纳入优化框架同时引入需求响应事件触发机制和碳配额约束让系统在运行成本最小化的前提下还能获得额外收益。它面向的读者是正在做能源管理系统、微电网调度或综合能源服务平台的开发者以及想了解这两个机制如何在真实系统中落地的从业者。项目运行下来给我最大的启发是需求响应和碳交易本质上都是把外部信号电价信号 / 碳价信号转化为系统内部决策变量的过程。代码怎么写直接决定了系统能不能真正听懂这些信号。2. 整体设计与思路拆解为什么要把 DR 和碳约束同时塞进优化模型2.1 单一目标优化的天花板很多早期的园区能源管理系统优化目标只有一个——购电成本最小。模型长这样目标函数min ∑(购电量 × 实时电价)约束条件功率平衡、设备出力上下限、储能SOC约束。这个模型跑起来很快用线性规划LP或者混合整数线性规划MILP都能解。但放到现在的政策环境下会出现几个尴尬场景场景A电网发布削峰需求电价尖峰时段高达1.8元/kWh但用户侧正好有可平移负荷比如空调预冷、热水蓄热单纯的经济优化模型会选择硬扛尖峰电价因为它不知道参与需求响应还可以拿补贴。场景B系统碳排放超额碳配额缺口的罚款成本高达每吨上百元但优化模型完全无视碳排放燃气轮机的出力可能因为天然气价格低而被过度调用导致碳排放量激增。场景C储能明明可以在低谷充电、高峰放电来套利但为了参与需求响应削峰需要在特定时段强制压低充电功率或增加放电深度如果模型不理解DR事件的收益结构就会白白损失这笔补偿。所以把需求响应和碳交易同时引入优化框架不是锦上添花而是让系统模型真正贴合现实约束的必要条件。2.2 模型选型混合整数线性规划的底气我最终选定的方案是MILP混合整数线性规划而不是纯LP也不是启发式算法比如粒子群、遗传算法。原因很务实需求响应事件具有0-1决策属性是否参与某次DR事件、是否开启可中断负荷都是离散决策LP天然搞不定必须引入整数变量。碳配额履约的阶梯惩罚是分段线性函数碳排放超出免费配额的部分按不同层级支付碳价这可以通过引入二进制变量和分段线性化Piecewise Linearization精确建模。MILP求解器成熟商业求解器如Gurobi、CPLEX对中等规模的MILP比如1000个约束、300个变量规模可以在秒级到分钟级内求解完全满足日内滚动优化的时间窗口要求。对比启发式算法MILP有一个不可替代的优势全局最优解的可证明性。粒子群算法跑出来的最优解可能是局部最优而能源调度涉及真金白银的交易决策我必须知道模型的解距离理论上限有多远否则没法向决策层汇报。2.3 统一的时序框架日内滚动优化 实时修正DR事件和碳配额结算都有明确的时间属性需求响应事件提前1天通知日前DR或提前几小时通知日内DR持续时间通常是1~4小时。碳配额履约按年度结算但在日内运行层面每天的碳排放都会累计到年度配额池里。所以整个系统采用双层时序框架日前层Day-ahead以24小时为窗口间隔1小时基于光伏/负荷预测数据制定次日的各设备出力计划同时接收DR事件的报价信息决定是否申报参与。日内层Intra-day以未来4~6小时为窗口间隔15分钟基于实测数据滚动修正设备出力确保日前计划的实际执行效果。这样做的好处是既保证了优化计算的实时性又保留了DR申报和碳约束这类慢变量的决策提前量。3. 核心需求响应机制落地从事件申报到补贴结算的代码视角3.1 需求响应的事件建模需求响应落实到代码层面核心是事件的抽象与注册机制。我把DR事件抽象成以下结构字段类型说明事件IDString唯一标识类型Enum削峰 / 填谷 / 紧急响应削减目标MWFloat要求削峰时减少的用电量申报截止时间Datetime最晚申报时间响应时段[start, end]事件持续时间窗口补偿单价元/kWhFloat按照实际削减量结算基线负荷kWhList事件时段的理论用电量基线基线负荷的计算是整个DR建模的敏感点。电网公司给的标准做法是事件前5个正常工作日同时段的负荷平均值但如果恰好那天有大设备启停或者其他异常操作基线会失真。我的做法是在代码里加一个数据清洗步骤剔除负荷序列中的离群点再结合天气相似日修正。这个清洗逻辑不复杂直接用3σ原则过滤掉偏差超过标准差3倍的采样点就行。3.2 参与决策的优化逻辑DR事件申报的本质是回答一个问题参与这个事件扣除对正常生产的干扰后综合收益是不是正的在MILP里这个决策被我建模成一个带收益项的目标函数增量目标函数新增项正收益项补贴收入 DR削减量 × 补偿单价。削减量 基线负荷 - 事件期间实际用电负荷。负收益项舒适度惩罚 若削减负荷导致部分设备输出低于最低需求用惩罚系数折算比如说空调负荷削减1 kWh对应室内温度超标产生的等效损失。关键约束是可削减负荷容量限制并不是所有负荷都能削。空调可以预冷蓄冷但核心生产的服务器不能随便断电。所以在负荷建模时我把负荷分成可中断负荷照明、空调、部分非关键工艺设备和不可中断负荷核心生产设备、医疗设备、数据中心机柜只有可中断负荷的潜力才能进DR申报池。这里有个踩过的坑DR补偿收益不能直接加进目标函数然后让求解器自由决策。原因在于求解器会觉得所有可中断负荷全削掉收益最大但这会让可中断负荷对应区域的温度失控、工艺参数漂移。我的解法是在每个可中断负荷的削减量上叠加一个舒适度/工艺损失系数损失系数随削减深度非线性增加——削减20%还好削减60%的损失是20%损失的三倍。非线性项不能直接进MILP所以我用分段线性化逼近实现很简单但效果明显。3.3 与储能策略的联动DR事件的正确打开方式其实是储能 可中断负荷的协同。举个例子某次削峰DR事件时段是14:00~16:00补偿单价1.2元/kWh。储能系统如果单独套利它会在14:00~16:00以尖峰电价1.5元/kWh放电这是它的最优策略。但加入DR收益后模型算了一笔账方案A不申报DR储能14:00~16:00放电300kWh收益300 × 1.5 450元方案B申报DR储能同样放电300kWh但这300kWh因为申报DR变成了响应削减量补贴收益为300 × 1.2 360元注意削减量是按基线负荷和实际负荷差计算的储能放电部分正好降低从电网购电的功率所以算削减量同时储能放电仍然降低了购电成本300 × 1.5 450元合计收益约810元。你看同样的一次放电行为因为嵌入了DR申报逻辑收益从450元变成810元。但实际核减还要看基线——如果基线计算方式不同结论会大幅波动。这就是为什么基线的算法细节决定DR的真实收益。代码实现上我在储能决策变量旁边加了一个辅助变量DR申报削减量约束是它不超过基线负荷与购电功率的实际差值然后把这个辅助变量乘以补偿单价放进目标函数。整个过程对储能约束完全透明——储能还是按电价差去充放电但模型会自动调整出力和申报量达到整体最优。4. 碳交易机制的技术耦合从配额分配到碳排放流追踪4.1 碳排放核算的边界问题碳交易落到综合能源系统第一个要搞清楚的问题是核算边界。园区外购电力对应的碳排放属于范围二的间接排放发电侧排放燃气轮机烧天然气属于范围一的直接排放。国内碳市场目前的趋势是把这两块统一管起来所以在系统里年度碳排放总量是一个汇总值。代码里的核算逻辑全年碳排放量 ∑(外购电量 × 电网排放因子) ∑(天然气消耗量 × 燃料排放因子) × 氧化率修正电网排放因子Emission Factor, EF很关键目前通常取全国电网平均排放因子每年更新一次单位是tCO₂/MWh。天然气排放因子大约是2.16 tCO₂/千标立方米不同的天然气热值取值略有差别。这两个因子要在系统配置里做成可配置参数因为政策每年都在调写死在代码里过几个月就要改。我在项目里是把排放因子放进了配置中心每年更新一次自动生效。4.2 碳配额约束进优化模型免费碳配额的计算方式我采用的简化做法是基准线法年度免费配额 基准年度的排放强度 × 连续生产周期折算的额定产能 × 行业基准排放基准值但为了模型不过于复杂我把碳配额履约直接转化为优化模型中的年度碳排放上限约束∑(逐时碳排放量) ≤ 年度配额 可购买的额外配额如果实际排放低于配额富余配额可以在碳市场挂牌出售产生收入。如果超过配额要么购买碳配额CCER或市场配额要么接受罚款。这个模型可以表述为目标函数新增项碳交易收益 (闲置配额量 × 碳市场挂牌价) —— 这是正收益购碳成本/罚款 (超额排放量 × 碳价/罚款单价) —— 这是负收益关键是给这个约束加一个阶梯罚函数超额排放一定量以内按碳市场价购买配额超出一定比例以上按政府罚款价执行罚款价通常高于市场价。这样模型会自动权衡是用燃气轮机多发电碳排放高还是从电网购更绿的电碳排放低但电价高本质上就是一个碳价动态引导能源结构切换的信号。4.3 实时碳流的追踪与记录优化模型的输出是应该怎么做但实际运行环节是实际做了什么这两个之间有偏差。偏差的来源包括预测误差光伏比预期少发了50kWh、设备响应延迟储能充电功率爬坡需要时间、通信丢包设备采集中断了15分钟。在代码架构里我把碳数据跟踪做得平行于能量流追踪每15分钟采集一次各设备的功率数据换算成当日累计碳排放。每日结算时把碳排放数据与当日DR申报记录做交叉校验。月度汇总后与碳配额预分配额度做差额告警。这块踩过一个实际的坑外购电力的碳排放因子不能简单地用年月的全国平均因子。因为日内不同时段电网中可再生能源的占比不同夜间风电大发的时段碳排放因子实际上低于午间光伏不足、火电顶峰的时段。如果统一用一个因子等于在碳排放核算上人为放大了某些时段的排放量。我的处理方式是采用电网公布的分时段排放因子表部分地区已经发布没有分时段数据时再用平均因子兜底。5. 实操过程与核心环节实现一套可以抄作业的MILP模型5.1 用Python Gurobi实现核心优化框架我用的环境是Python 3.10 Gurobi 10.x建模语言是Gurobi Python API。下面的代码是一个简化但完整可运行的框架我逐步拆解每一步的意图。import gurobipy as gp from gurobipy import GRB # 基础数据假设 hours 24 price [0.3] * 8 [0.8] * 8 [1.5] * 4 [0.6] * 4 # 峰平谷电价 load [200] * 8 [350] * 8 [500] * 4 [280] * 4 # 负荷预测 pv_power [0]*6 [80, 150, 200, 220, 180, 100, 50] [0]*11 # 光伏出力简化 pg_fee [600] * 24 # 电网购电上限单位kW # 碳排放参数 grid_ef 0.581 # 电网排放因子 t/MWh gas_ef 2.16 # 天然气排放因子 t/kNm3 gas_price 0.13 # 天然气单位成本 # DR事件参数 dr_hours [14, 15, 16, 17] dr_price 1.2 # 元/kWh baseline_load [500] * 4 # DR基线负荷事件时段 # 创建模型 m gp.Model(IES)接下来的核心是定义决策变量# 决策变量 buy_power m.addVars(hours, vtypeGRB.CONTINUOUS, lb0, namebuy_power) # 从电网购电 ch_power m.addVars(hours, vtypeGRB.CONTINUOUS, lb0, namecharge) # 储能充电 dis_power m.addVars(hours, vtypeGRB.CONTINUOUS, lb0, namedischarge) # 储能放电 soc m.addVars(hours, vtypeGRB.CONTINUOUS, lb0, ub200, namesoc) # 储能SOC容量200kWh gas_power m.addVars(hours, vtypeGRB.CONTINUOUS, lb0, ub300, namegas_power) # 燃气轮机出力 gas_fuel m.addVars(hours, vtypeGRB.CONTINUOUS, lb0, namegas_fuel) # 燃气消耗量 dr_declare m.addVar(vtypeGRB.BINARY, namedr_declare) # 是否参与DR dr_amount m.addVars(4, vtypeGRB.CONTINUOUS, lb0, ub100, namedr_amount) # DR时段削减量注意几个关键设定储能SOC的递推约束用逐时累加表达我加了充电效率0.95、放电效率0.95。buy_power的功率平衡方程负荷需求、储能充放电、光伏、燃气轮机的出力必须刚好平衡这是综合能源系统模型的灵魂约束。# 功率平衡约束 for t in range(hours): m.addConstr( buy_power[t] pv_power[t] gas_power[t] dis_power[t] load[t] ch_power[t], namefbalance_{t} ) # 储能SOC递推 m.addConstr(soc[0] 100, namesoc_init) for t in range(1, hours): m.addConstr( soc[t] soc[t-1] ch_power[t]*0.95 - dis_power[t]/0.95, namefsoc_{t} ) # 燃气轮机与燃料转换 for t in range(hours): m.addConstr(gas_fuel[t] gas_power[t] / 3.5, nameffuel_{t})5.2 需求响应事件约束的代码实现这段是DR进入模型的精华部分我用辅助0-1变量把DR决策和削减量解耦# DR事件时段约束 for i, t in enumerate(range(14, 18)): # 削减量不能超过基线负荷与购电量的差值即实际从电网净购电降低的量 m.addConstr( dr_amount[i] baseline_load[i] - buy_power[t] (1 - dr_declare) * 500, namefdr_limit_{i} ) # 不参与时削减量为0 m.addConstr( dr_amount[i] dr_declare * baseline_load[i], namefdr_declare_{i} ) # 同时DR削减量不能超过可中断负荷潜力 m.addConstr( dr_amount[i] 100, namefdr_potential_{i} )5.3 碳配额约束和碳交易项# 碳排放总量 购电排放 燃气排放 carbon_total gp.quicksum(buy_power[t] * grid_ef for t in range(hours)) / 1000 carbon_total gp.quicksum(gas_fuel[t] * gas_ef for t in range(hours)) / 1000 # 年度配额按日折算 daily_quota 3.5 # 吨/日 carbon_surplus m.addVar(lb-10, ub10, namecarbon_buy) # 正值表示富余负值表示缺口 m.addConstr(carbon_total carbon_surplus daily_quota, namecarbon_balance) # 碳价参数 carbon_price 62 # 元/吨 carbon_penalty 89 # 元/吨碳罚款注意carbon_surplus我给了上下界意味着模型可以从碳市场购买/出售配额但交易量有上限符合实际市场的流动性约束。5.4 目标函数把三类收益全部兑到一起# 目标函数总成本最小化 obj gp.LinExpr() # 1. 购电成本 for t in range(hours): obj buy_power[t] * price[t] # 2. 燃气成本 for t in range(hours): obj gas_fuel[t] * gas_price # 3. DR收益负号进入成本 for i in range(4): obj - dr_amount[i] * dr_price / 1000 # kWh转为MWh统一单位 # 4. 碳交易收益 obj - carbon_surplus * carbon_price m.setObjective(obj, GRB.MINIMIZE)这里的单位需要格外注意电量的基准单位不统一是能源系统建模中最容易翻车的细节之一。我这里买电和DR都用kWh碳排放用的吨碳价是元/吨如果电量单位换算错一位目标函数里的权重就会差一个数量级模型跑出来当然一团糟。建议开局就用MWh做统一单位排放因子也用t/MWh。5.5 求解输出与决策转译m.optimize() # 输出关键结果 if m.status GRB.OPTIMAL: print(f总成本: {m.objVal:.2f} 元) print(fDR申报削减量: {sum(dr_amount[i].X for i in range(4)):.2f} kWh) print(fDR收益: {sum(dr_amount[i].X * dr_price for i in range(4)):.2f} 元) print(f碳余额: {carbon_surplus.X:.3f} 吨)模型跑完后最优解会自动告诉我今天参不参与DR、参与的话削减多少负荷、储能什么时段充放、燃气轮机要不要开。把这些决策下发到设备控制层就是执行层的事了。5.6 约束松紧程度的调试心得刚开始跑模型时我遇到一个异常现象DR申报的削减量远大于可中断负荷的真实潜力。查了很久才发现问题出在基线负荷的定义上。DR事件时段是14:00~17:00但我在第3.1节的边界条件里把削减量不能超过基线负荷与购电量的差值写成了购电量可以直接降为零而实际上园区有不可中断负荷电网购电量有硬下限。加了不可中断负荷的功率下限约束后DR削减量立刻收敛到了合理值。这个坑提醒我需求响应的削减潜力必须用真实可中断负荷 储能放电能力 分布式电源增发能力来约束而不是用理论最大购电功率做减法。模型里如果少一个功率下限约束结果会偏离实际很远。6. 常见问题与排查技巧实录那些坑我替你踩过了6.1 求解器报infeasible model怎么办模型不可行的最常见原因是约束自相矛盾。我在测试阶段遇到过DR削减量约束和功率平衡约束打架的情况DR削减量要求购电量降得很低但储能SOC已经见底燃气轮机上限也不够同时不可中断负荷的功率下限又很高三条约束互相牵扯系统找不到可行解。排查思路是逐条注释掉DR相关约束看模型是否恢复可行先注释碳约束验证基础能量平衡模型是否可行再注释DR削减量验证去掉DR后碳约束是否可行如果去掉了DR约束模型可行说明DR削减量与储能/燃气不足之间存在冲突需要在约束里增加削减量的动态上限比如削减量不超过当前时段可调节裕度之和。有些项目会用Gurobi的IISIrreducible Inconsistent Subsystem功能自动找最小不可行约束集这在调试阶段非常好用:m.computeIIS() for c in m.getConstrs(): if c.IISConstr: print(f冲突约束: {c.constrName})6.2 环比训练集DR基线的重复博弈问题DR基线在真实运行中还有一个麻烦基线是用历史负荷算的如果用户每次都刻意在基线计算时段抬高用电基线就会越抬越高削弱DR的实际效果。这在行业里叫基线博弈。代码层面我加了基线剔除规则如果某天负荷在基线计算时段出现异常峰值高于前7天平均值的1.5倍该天不作为基线参考日对参考日的负荷做日归一化消除整体负荷水平偏移的影响最终基线 最近5个有效参考日的时段均值剔除一个最高一个最低再平均。这套规则写起来很简单但能显著缓解用户在基线周期蓄意抬高负荷的博弈行为。6.3 碳约束的时序匹配问题碳配额是年度总量约束但优化模型是日内滚动。必须把年度配额按比例拆分到日不能拿全年配额去卡每一天。我在项目里做了两个层级年度级配置全年配额总量月底做累计预警。日级别按预测产能把配额拆分到每日日内调度时用日配额做硬约束。日配额也不能是一成不变的固定值。如果某天工业园区因大检修产量骤降日配额对应也要下调否则今天多余的配额被浪费了明天实际生产恢复时却因为配额硬约束而被迫减少燃气轮机出力。我的处理方法是按7天滚动平均产能来动态拆分配额而不是固定平均到365天。6.4 求解时间与预测误差的平衡MILP模型一旦加入DR申报变量和碳约束如果预测误差叠加导致的约束频繁失效很容易出现求解器反复迭代。我做了两个调整决策变量优先级分层储能SOC和燃气轮机功率作为第一优先级决策DR削减量作为第二优先级。先固定第一优先级再在剩余空间里优化DR申报量。这虽然牺牲了全局最优性但求解时间从3分钟降到15秒。预测误差的鲁棒性光伏和负荷预测误差超过15%时切入夜间模型或极端场景约束比如把光伏出力削减20%再优化一次。这样处理后日内的15分钟滚动窗口完全可以承受2~3次完整优化求解不会拖垮实时控制栈。7. 后续扩展建议这套模型还能往哪里生长项目做到这个阶段我意识到综合能源系统的优化远不止维护复杂模型这么简单它的价值在于不断接入新的外部信号源把系统的响应维度撑开。7.1 现货市场与实时电价的深度融合目前电价用的是峰谷平三段固定价格但很多省份已经推出电力现货市场电价每15分钟波动一次。如果把DR基线的计算精度上调到15分钟粒度让储能策略跟随现货电价曲线动态调整系统的套利空间会明显扩大。代码里的接入点就是价格参数从静态数组变成动态读取的接口函数目标函数和约束完全不用大改。7.2 绿证与碳配额的联动机制绿证绿色电力证书和碳配额之间有一个联动逻辑购买绿电可以抵消部分碳排放。具体到模型里就是把购电量 × 绿电比例在碳排放核算中扣减。我在系统里预留了绿电比例这个可配置参数未来政策细则确定了就放开这个阀值。7.3 多园区协同的分布式优化单个园区的优化做到极致后下一步是多个园区之间的协同。两个园区共享一台储能、或者互相调剂可中断负荷的DR申报量这就涉及多智能体博弈或分布式优化的框架了。我在模型架构上预留了跨园区约束接口但还没开展到那个阶段等有更多实测数据再补这部分的分享。最后分享一个运行中的小技巧模型跑出来的结果不要直接下发控制层先做一分钟的人工规则校验。比如模型可能建议在某一时刻储能同时充电和放电虽然目标函数会防止这种荒谬解但数值容差下偶尔会出现这时候加上充放电互斥约束ch_power × dis_power 0或引入0-1变量可以杜绝这类异常。我遇到过两次后来直接把互斥约束作为默认约束常驻模型里了。能源系统的调度优化到了这个层面已经完全不是节电两个字能概括的了。需求响应把用户变成了电网的调节资源碳交易把环境成本变成了经济信号两股力量一起涌进优化模型代码就是它们交汇的地方。希望这套拆解能对正在做类似项目的你有帮助少踩几个我已经替你踩过的坑。
阅读完成 · 觉得有帮助?
咨询建站