YAOTU INSIGHTS

分段损耗与阶梯碳价下的多源协同储能优化调度

分段损耗与阶梯碳价下的多源协同储能优化调度
分段损耗、需求侧响应、阶梯碳价、多源协同、储能优化这五个词拆开看都是能源系统优化的老熟人但揉进同一个模型再用Python落地才是真正的坑点所在。我做这个项目的初衷很简单手头有一个园区级多能互补系统的调度问题风、光、火、储、网购电全在一张网里原方案用的是固定效率系数、固定碳价、无需求响应结果算出来的调度策略不仅保守而且一到峰谷转换时段就出现“既充电又放电”的诡异操作。后来我参考了几篇文献里“分段损耗阶梯碳价需求侧响应”的思路自己动手改了一版代码效果提升非常明显于是把它整理成了这份原创改进实现。这篇文章适合三类人看一是正在做碳电耦合调度方向的学生需要一个可以直接跑通、方便改参数的MILP参考实现二是搞园区能源管理的工程师想评估“储能需求响应阶梯碳价”能不能降本三是刚接触能源系统优化、想知道分段线性化到底怎么落到代码里的Python初学者。我会把关键数学表达、线性化技巧、代码骨架、调参经验和踩过的坑都讲清楚所有内容基于我在实际跑模型时的经验复盘不涉及具体项目数据但结构和参数设置可以直接迁移到你自己的场景里。1. 为什么要把四件事揉在一个模型里这个组合不是一时兴起而是环环相扣的。分段损耗解决的是“储能系统怎么建模才更接近物理现实”的问题需求侧响应解决的是“负荷侧有没有资源可挖”的问题阶梯碳价解决的是“碳排放成本怎么动态反映政策压力”的问题多源协同则是把这些要素放进一个统一优化框架里让风、光、火、储在同一个目标下博弈。1.1 分段损耗从“一个效率常数”到“一条分段曲线”很多入门级的储能调度模型充放电损耗就写死一个系数比如充电效率0.95、放电效率0.95然后一路用到底。这个做法最大的问题在于它假设效率与功率区间无关但实际上储能系统的效率曲线是典型的“低功率区间偏低、额定功率区间最高、过载区间又掉下来”的形状具体和变流器、电池内阻、温度都有关系。用一个恒定效率拟合整条曲线在低功率策略占优的场景下模型会高估实际充电量导致调度结果偏乐观。我在改进代码里用了一个分段线性化的思路把充电功率分成几个区间每个区间给一个不同的效率斜率。这样损耗就不是一个常数而是一段折线每个分段对应一个边际损耗系数。分段损耗的好处是它既保留线性规划的求解效率又能贴近真实的非线性效率曲线。用生活化一点的话说你手机充电快充阶段接近满功率线损相对小涓流充电阶段虽然功率低但转换效率其实也不高。一条直线没法刻画这两头的差异两条、三条折线才能。1.2 需求侧响应让用户不再是“被动负荷”需求侧响应DR本质上是把负荷侧的可调节资源显式建模进优化问题。我在模型里区分了两类DR资源可平移负荷和可削减负荷。可平移负荷像工厂里的某条产线它整体必须在某个时段窗口内完成但具体安排在哪个时段是决策变量可削减负荷则更像空调、照明这类允许在高峰时段降低一部分用电量但代价是支付给用户的补偿费用。引入DR之后系统调度员手里就多了一张牌电网高峰时段可以先让储能放电同时削减一部分可削减负荷再把可平移负荷挪出峰段三者共同作用减少向网购电的压力。DR和储能是天然互补的储能响应快但容量有限DR响应慢一点但持续时间可以很长把它们放在一个模型里同时优化能比单独使用任何一种方式节省更多运行成本。1.3 多源协同与阶梯碳价为什么一起出现多源协同听起来很高大上落到代码里其实就是把各种电源和储能的出力变量放进同一组功率平衡约束用一个混合整数线性规划MILP求解。火电作为可控机组承担主要的碳排责任风电、光伏作为不可控新能源优先消纳储能作为缓冲平抑净负荷波动网购电则代表与外部电网的交互。这些单元不协同调度就会出现“火电满发、新能源弃电、储能空置”的资源错配。阶梯碳价则是我在这版改进代码里最想强调的一个点。传统模型里碳价是固定的一吨碳多少钱就是多少钱但实际碳市场的价格机制往往是分档的碳排放量在配额内是一个价格超过配额进入另一个价格段再超再涨。这就是阶梯碳价。把阶梯碳价写进目标函数之后火电出力不再只看煤耗成本还要看它把系统碳排放量推到了哪个区间。碳排越高边际碳成本越高优化器就会主动降低火电出力、增加新能源和储能的使用。这就是碳价信号在“指挥”系统做低碳调度。2. 优化模型的核心数学表达这个模型本质是一个带0-1变量的混合整数线性规划MILP下面我把目标函数、分段损耗线性化技巧和约束条件逐个拆开讲尽量用容易理解的方式说清楚。2.1 目标函数成本最小还是碳排放最小我的目标函数是“运行成本最小化”但这里的成本包含四块购电成本、火电燃料成本、碳成本和需求响应补偿成本。碳成本不是简单的一个线性项而是与总碳排放量挂钩的阶梯函数。用数学语言写就是minimize: sum( t, 购电电价[t] * 购电功率[t] ) sum( t, 燃料成本系数 * 火电出力[t] ) 碳成本(总碳排放, 阶梯区间) sum( t, 可削减负荷补偿单价 * 削减量[t] ) sum( t, 可平移负荷补偿单价 * 平移操作标记[t] )碳成本这一项是整个建模的难点。总碳排放来自火电出力和网购电对应的间接排放如果电网排放因子已知可以折算进来。模型需要先算总碳排再判断它落在哪一个碳价阶梯区间然后对不同的区间部分分别计价。问题在于“落在哪个区间”是一个逻辑判断需要0-1变量来实现。2.2 线性化技巧分段损耗的增量成本法分段损耗的线性化我用的是“增量成本法”也叫λ表示法。思路是把充电功率划分成K个分段每一段引入一个连续变量λ_k表示该分段被使用的比例同时引入0-1变量z_k保证分段必须从小到大依次使用。举个例子假设充电功率上限是500kW分成三段0-200kW200-350kW350-500kW每段对应的损耗系数分别是0.90、0.94、0.91这里只是示例实际需要根据效率测试曲线标定。线性化约束的核心代码如下def piecewise_loss_model(Model, Pmax, breakpoints, eta_seg, namecharge): # breakpoints: [0, 200, 350, 500] # eta_seg: {0: 0.90, 1: 0.94, 2: 0.91} # 返回一个函数给定总功率返回对应的“损耗功率” lambdas {k: Model.add_var(namef{name}_lambda_{k}) for k in range(len(breakpoints)-1)} z {k: Model.add_var(namef{name}_z_{k}, var_typeB) for k in range(len(breakpoints)-1)} # 总功率 各分段功率之和 Model.add_constr(sum(lambdas[k] * (breakpoints[k1] - breakpoints[k]) for k in lambdas) Pmax) # 连续性约束前一段必须用满后一段才能用 for k in range(len(breakpoints)-2): Model.add_constr(lambdas[k] z[k1]) # 每段占比不超过1 for k in lambdas: Model.add_constr(lambdas[k] 1) # 损耗功率 Σ λ_k * 分段长度 * (1 - eta_k) loss sum(lambdas[k] * (breakpoints[k1] - breakpoints[k]) * (1 - eta_seg[k]) for k in lambdas) return loss这里z变量只做“顺序使用”约束实际求解中如果你不关心哪段先用可以简化掉一部分变量但对于储能这种需要精确刻画效率曲线的场景完整保留z变量更稳。我一开始偷懒省略了连续性约束结果优化器经常出现“跳区间使用”的情况明明200kW以内的损耗更低它却优先用第三段这显然不符合物理常识。2.3 约束条件的“全家桶”模型里的约束可以分成四类。第一类是功率平衡约束每个时段通常取24h或96h步长为1h或15min内火电出力 风电出力 光伏出力 储能放电 购电功率 可削减负荷减少量 基础负荷 储能充电 可平移负荷增加量。第二类是储能运行约束包括充电功率上限、放电功率上限、SOC递推公式、SOC上下限。SOC递推是SOC[t1] SOC[t] 充电功率[t] * 充电损耗系数 - 放电功率[t] / 放电损耗系数注意充电和放电不能同时进行这一步我用一个0-1变量来做互斥约束。第三类是需求响应约束可平移负荷的累计平移量必须归零、可削减负荷的削减量有上下限。第四类是阶梯碳价约束需要引入多档0-1变量来定位碳排放量所在的碳价区间碳成本在这个区间内是分段线性的。3. Python实现的分步拆解这一部分是文章的重头戏我按一个能直接跑的工程来拆解整个过程从工具选型、数据准备到核心代码结构和可视化验证一条线走下来。3.1 环境准备和求解器选型在开始写模型之前先把Python环境搞定。建议使用Python 3.9以上版本我本地用的是3.10配合pip安装依赖就能运行。如果你对Python安装、环境配置还不太熟悉可以先用官方安装包装好Python再加系统环境变量然后在终端用pip安装下面的库这一步是所有优化模型的基础环境不顺的话后面寸步难行。求解器选型上我对比过开源和商业两个方向。单纯跑通、验证算法逻辑用免费的mip库默认CBC求解器足够了但如果你的问题规模很大、分段很多、时间尺度到96点甚至168点CBC的求解速度会明显下降这时建议换成Gurobi它在处理大规模MILP问题上确实快很多。我的做法是代码层统一用mip库来写模型因为它的API精简、上手成本低后期要切换求解器也方便。核心依赖就两个mip用于建模求解matplotlib用于画调度图和碳排放阶梯图。pip install mip matplotlib pandas3.2 数据准备与场景设计数据准备是整个环节里最容易被低估的部分。我自己跑下来发现模型结果好不好看80%取决于输入数据是否合理。以下是我在测试场景里用的一组典型数据你可以直接抄来跑第一版再根据自己的项目替换风电、光伏、负荷曲线。参数名数值说明典型日调度时段24点步长1h也可以改成96点做日内滚动基础负荷峰值1200 kW取自园区典型日负荷曲线风电装机400 kW测试场景中按低出力场景设置光伏装机500 kW光照时段集中在8:00-17:00火电最小出力100 kW低于此值不启机火电最大出力600 kW分三档报价储能容量800 kWh对应4小时容量配置储能充放功率上限200 kW采用同样的功率上限便于对比购电电价峰值/谷值1.2 / 0.4 元/kWh采用分时电价峰段为10:00-15:00和18:00-21:00还有个特别容易忽略的点碳配额基数和阶梯碳价区间。我在场景里设置免费碳配额为8000kg如果总碳排放量在配额以内碳价为0超过配额的部分0-5000kg按0.08元/kg计5000-10000kg按0.15元/kg计超过10000kg按0.25元/kg计。这个阶梯设计直接影响火电的出力行为配额设得太松碳价基本不起作用设得太紧火电直接不发了也不现实。3.3 核心代码实现储能、DR与阶梯碳价下面我会按模块给出关键代码骨架。这些代码不是完整的可直接运行脚本但核心逻辑是齐全的你可以在此基础上补上数据读取和循环拼出完整模型。先看储能模块重点是SOC递推和充放电互斥约束from mip import Model, xsum, minimize, BINARY, CONTINUOUS m Model(multi_source_co_optimization, solver_nameCBC) # 时间点 T range(24) # 决策变量 p_fire {t: m.add_var(var_typeCONTINUOUS, lb100, ub600, nameffire_{t}) for t in T} p_wind {t: m.add_var(var_typeCONTINUOUS, lb0, ubwind_forecast[t], namefwind_{t}) for t in T} p_pv {t: m.add_var(var_typeCONTINUOUS, lb0, ubpv_forecast[t], namefpv_{t}) for t in T} p_buy {t: m.add_var(var_typeCONTINUOUS, lb0, ub800, namefbuy_{t}) for t in T} p_ch {t: m.add_var(var_typeCONTINUOUS, lb0, ub200, namefch_{t}) for t in T} p_dis {t: m.add_var(var_typeCONTINUOUS, lb0, ub200, namefdis_{t}) for t in T} u_ch {t: m.add_var(var_typeBINARY, namefu_ch_{t}) for t in T} u_dis {t: m.add_var(var_typeBINARY, namefu_dis_{t}) for t in T} soc {t: m.add_var(var_typeCONTINUOUS, lb0.1, ub0.9, namefsoc_{t}) for t in T} # 充放电互斥 for t in T: m.add_constr(p_ch[t] 200 * u_ch[t]) m.add_constr(p_dis[t] 200 * u_dis[t]) m.add_constr(u_ch[t] u_dis[t] 1) # SOC递推容量800kWh基准取SOC占比 for t in T: if t 0: m.add_constr(soc[t] 0.2 (p_ch[t] * 0.94 - p_dis[t] / 0.94) / 800) else: m.add_constr(soc[t] soc[t-1] (p_ch[t] * 0.94 - p_dis[t] / 0.94) / 800)SOC初始值设为0.2日末我一般要求SOC回到0.2这样可以保证“日清日结”第二天调度不受前一天影响。很多初学者忘掉末尾SOC约束算出来的方案看着便宜其实是在“吃电池老本”不可持续。再看阶梯碳价的实现。这一块我采用两组0-1变量定位碳排区间再用“区间选择”约束来写碳成本# 碳排放总量简化只算火电购电间接排放 total_emission xsum(fire_co2_rate * p_fire[t] grid_co2_rate * p_buy[t] for t in T) # 阶梯区间定位变量 z1 m.add_var(var_typeBINARY, namecarbon_z1) # 是否超过配额 z2 m.add_var(var_typeBINARY, namecarbon_z2) # 是否超过配额5000 z3 m.add_var(var_typeBINARY, namecarbon_z3) # 是否超过配额10000 # 配额、区间长度 quota 8000 len1 5000 len2 5000 # 逻辑关系z1 z2 z3且对应区间上限约束 m.add_constr(z1 z2) m.add_constr(z2 z3) m.add_constr(total_emission quota len1 * z1 len2 * z2 len1 * z3 1e5 * (1 - z3)) # 碳成本 0*配额内部分 0.08*区间1 0.15*区间2 0.25*区间3 extra1 m.add_var(nameextra1) # 超过配额但不超过配额5000的部分 extra2 m.add_var(nameextra2) extra3 m.add_var(nameextra3) # 用分段线性分解碳排放量 m.add_constr(total_emission quota extra1 extra2 extra3) m.add_constr(extra1 len1 * z1) m.add_constr(extra2 len2 * z2) m.add_constr(extra3 len1 * z3) carbon_cost 0.08 * extra1 0.15 * extra2 0.25 * extra3这里用了一个小技巧total_emission quota extra1 extra2 extra3把总碳排拆成三块z1/z2/z3决定哪一块可以被使用。比如总碳排只有9000kg那么extra11000extra20extra30碳成本就是0.08*1000。这样处理的好处是MILP结构干净不需要大M断点太多求解速度比传统“区间大M法”更快。大M值我取1e5实测下来CBC也能较快收敛但在Gurobi里建议把大M调小一点比如设成总碳排上限*1.5能显著减少数值病态。这种“连续变量阶梯定位0-1变量”的组合是阶梯碳价建模的核心。很多人喜欢用“两两互斥的0-1变量”就是每个区间一个z互斥约束z1z2z31但当区间超过3档时这种写法会产生大量冗余组合效率远不如“单调累加式”的z变量设计。这一点我在多次调试后确认了推荐优先使用累加式。3.4 需求响应约束的建模细节需求响应模块我分成可削减负荷和可平移负荷两类来建模。可削减负荷相对简单每个时段有一个削减量变量cut[t]上下限约束后目标函数加一项削减补偿成本。可平移负荷复杂一些需要记录“平移出去”和“平移进来”两个动作并且保证一天内平移总量为0# 可削减负荷每时段最多削减基础负荷的15% cut {t: m.add_var(var_typeCONTINUOUS, lb0, namefcut_{t}) for t in T} for t in T: m.add_constr(cut[t] 0.15 * base_load[t]) # 可平移负荷move_out[t]表示把负荷移出t时段move_in[t]表示移入t时段 move_out {t: m.add_var(var_typeCONTINUOUS, lb0, ub50, namefmo_{t}) for t in T} move_in {t: m.add_var(var_typeCONTINUOUS, lb0, ub50, namefmi_{t}) for t in T} # 当天平移总量守恒 m.add_constr(xsum(move_out[t] for t in T) xsum(move_in[t] for t in T)) # 任意时刻净负荷 基础负荷 - cut - move_out move_in net_load {t: base_load[t] - cut[t] - move_out[t] move_in[t] for t in T}可平移负荷补偿成本的计算要小心是按“发生平移动作的次数”收费还是按“平移电量”收费我在项目里选择的是按电量收费也就是move_out或move_in的绝对值乘以补偿单价。原因是按次数收费需要额外引入0-1变量判断“有没有发生平移”会进一步增加整数变量规模对于24时段模型还能忍受但如果扩展到96点求解时间会明显增加。按电量收费虽然单价上调了一些但模型还是保持纯线性结构性价比高很多。3.5 结果可视化与模型合理性验证模型跑完之后我第一件事不是看总成本而是画三个关键图各电源出力堆叠图、储能SOC曲线、碳排放阶梯位置图。这三个图能快速判断模型是否“物理上合理”。比如如果SOC曲线一天内剧烈振荡、频繁抽放那大概率是充放电互斥约束失效或者损耗系数写错如果火电在某些时段明显高于上网电价对应的边际成本要检查碳价是否把火电压得太狠如果DR削减出现在谷段而不是峰段那补偿单价肯定标错了因为谷段不需要削减负荷。画图用matplotlib就够了。堆叠图用plt.stackplotSOC曲线用普通折线图阶梯位置图可以用柱状图标出碳排放总量和三条分界线。看清这三个图模型有没有问题基本一目了然。4. 常见问题与排查技巧实录每个人跑优化模型都会遇到几个“经典难题”这里我把自己的踩坑记录整理成速查表附上排查思路和解决建议希望对你能有帮助。问题现象可能原因解决办法模型求解时间极长数小时不收敛0-1变量过多减少时间点数量压缩分段数量固定部分无需变化的开关变量换Gurobi储能SOC频繁振动充放电互斥约束缺失或损耗系数符号错误检查是否有u_ch u_dis 1检查SOC递推的加减符号碳价不起作用火电照常满发碳配额设得过高或碳价区间设得过高调低配额观察碳排放总量落在哪个区间提高阶梯碳价单价DR削减发生在谷段补偿单价和分时电价不平衡谷时段购电成本低削减不划算检查补偿单价是否低于谷段电价总碳排越界模型无解阶梯区间约束写死无法表达超过上限的情况增加一个“无限区间”或给最后一个区间设很大的上限结果出现同时购电和火电满发功率平衡约束漏掉或上网电价与燃料成本不匹配检查净负荷公式检查是否漏了购电上限约束4.1 代码慢的根源整数变量太多我第一版改进代码用的是96点时间分辨率每个时间的储能充放互斥、阶梯区间定位、DR动作标记零零总总加起来接近300个0-1变量。CBC在默认参数下跑了20分钟都出不了可行解后来我做了三处优化一是把时间分辨率降到24点先验证算法正确性再考虑细化二是阶梯碳价从“每区间独立一个变量”改成“单调累加式变量”减少了至少一半的0-1变量三是把DR可平移负荷的0-1标记去掉只用连续变量和补偿单价来驱动。三招下来同一个模型求解时间从“无解”降到40秒内效果立竿见影。4.2 阶梯碳价的“跳档陷阱”阶梯碳价天然是一个非凸函数如果不小心把“总碳排放量配额第一段第二段”的总量等式和多档0-1变量组合在一起很容易出现“碳排明明只有9000kgextra2却分到2000kg”的离奇结果。根本原因是缺少区间约束extra2只有在z21时才能使用但如果没有上限约束优化器可能为了压低碳成本而把额外的碳排放“塞”到更便宜但本不该使用的区间里。解决办法就是在每个extra变量上都加上限约束并让z变量单调累加。这也是我在3.3节代码里写extra2 len2 * z2的原因每一个extra都必须严格绑定对应的区间定位变量才不会有“跨区间逃逸”的漏洞。4.3 需求响应补偿单价怎么定才合理DR补偿参数设得不对模型结果会非常离谱。我的调参经验是从一个基准价格出发让“削减1kW电量的补偿成本”约等于“峰段购电电价的60%-80%”。比如峰段电价1.2元/kWh那削减补偿可以设在0.7-0.9元/kWh这样模型才会在峰段主动削减负荷同时谷段不会乱削减。如果设得太高模型会把所有能削的负荷全削掉相当于用DR强行“假减排”设得太低DR模块形同虚设和没加DR一样。可平移负荷的补偿单价则可以更低一点因为用户的实际不适感比削减负荷小我一般设在0.3-0.5元/kWh。4.4 储能一天跑不了一圈SOC末端约束的隐形坑末尾SOC恢复约束是最容易被忽略、又最影响结果质量的约束。如果不加这个约束优化器会把储能容量当作“免费电池”在最后一个时段把SOC放空表面成本很低但无法用于第二天的重复调度。我项目里采用的做法是要求SOC[23] SOC[0]即调度的最后时刻回到初始荷电状态。这对连续多日滚动调度尤其重要能保证每一天都在公平的初始条件下做优化不会出现“第一天猛赚、后面几天亏光”的假象。5. 后续还能怎么扩展如果你已经把这版模型跑通了我还有三个扩展方向值得尝试。第一个是加入新能源出力的不确定性把风电和光伏的随机波动用场景法或鲁棒优化来处理第二个是把单日优化扩展成多日滚动优化让储能在周末和周一之间也能跨天调度第三个是把阶梯碳价替换成带交互影响的碳市场出清模型让园区从碳价的“接受者”变成“参与者”。这些方向都能在这份代码的基础上往下走。我个人在实际操作中的体会是这种多目标、多约束的优化模型难点从来不是数学公式而是“怎么用代码把物理逻辑表达得既准确又高效”。分段损耗、阶梯碳价这些概念理解起来并不难难的是在MILP框架里用0-1变量把非线性的现实世界“翻译”成线性约束再一步步通过调试让结果符合物理直觉。如果你也在改类似的储能与碳价协同优化模型希望这份改进代码和踩坑记录能帮你少走点弯路。最后再分享一个小技巧每次改完参数不要只盯着总成本看先把“储能SOC曲线”和“碳排放落点区间”打印出来过一遍。这两个指标是最诚实的它们不正常总成本再好看也是错的。先保证调度策略“物理上说得通”再追求经济上最优这个顺序千万不要搞反。