ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

风光储互补调度实战:Python建模与电池-抽蓄协同

风光储互补调度实战:Python建模与电池-抽蓄协同 风光储互补调度这个方向这几年在电力系统优化圈子里热度一直很高。尤其是“风电光伏储能”这种组合挨个拿出来都不算新东西可一旦把它们塞进同一个调度模型里要考虑的问题就完全不一样了。我自己做过的实际项目中最常见的痛点是三个一是风光出力不确定性怎么在调度里体现二是储能尤其是电池该按什么规则充放才能既不伤寿命又不浪费容量三是抽水蓄能这种“大块头”和电池这种“小快灵”在时间尺度上怎么配合。再加上“废弃矿井改抽蓄”这个很有意思的落地方向整套研究的建模复杂度、代码实现难度都上来了。这篇文章就把我在这个方向上的研究思路、Python建模过程、调试经验和踩过的坑摊开讲一遍。内容偏实战适合正在做风光储调度、微电网优化、或者对抽水蓄能改造项目感兴趣的研究生和工程师参考。我会把模型怎么搭、约束怎么写、参数怎么调、代码怎么组织都讲清楚争取让你看完就能搭出一个能跑的调度框架。1. 这个研究到底在解决什么问题1.1 风光出力的“不可控”才是调度的根源风电和光伏的出力曲线说白了是“看天吃饭”。风电出力受风速影响光伏出力受辐照度影响两者不仅随机波动大还会出现明显的时空错位——白天光伏出力峰值和傍晚负荷峰值常常对不上夜间风电出力高但负荷又下去了。这种错位会导致两个直接后果一个是弃风弃光另一个是系统频率和电压的波动加大。调度模型要解决的核心问题就是在给定风光出力预测曲线、负荷预测曲线、以及储能设备状态的条件下决定每一个时段里电池该充还是该放、抽蓄该抽水还是该发电、火电或者外购电该出多少力。这个决定要满足一个目标——比如系统运行成本最小或者弃风弃光率最小或者综合起来两者兼顾。实际做的时候我一般是把目标设成“运行成本最小化”然后把弃风弃光折算成惩罚项加进去。这样模型会自发地优先消纳风电光伏而不是一窝蜂地全上便宜电也不至于为了多存电把储能设备耗损得过分厉害。1.2 电池和抽蓄时间尺度上的互补搭档电池储能和抽水蓄能放在同一个调度模型里很多人第一反应是“都是储能功能重复了”。但真正放在运行环境下看两者是完全互补的。电池的特点响应快秒级到分钟级能量密度高适合做短时功率调节和日内能量搬移。缺点是容量成本高循环寿命有限频繁深度充放会快速衰减。抽蓄的特点响应慢分钟级功率大能量释放时间长适合做小时级甚至跨日级的能量调度。缺点是选址受限建设成本高。但如果是废弃矿井改造场地成本大幅降低地理条件反而成了“变废为宝”的存量资产。我在项目里就把电池当作“缓冲器”负责分钟到小时级的功率平滑和调峰把抽蓄当作“吞吐量大的仓库”负责日内的长时间尺度的能量搬移。两者配合比单独用任何一种都稳。这也是为什么这个调度研究的模型里两种储能的约束要分开写、分开调参——因为它们的时间常数和运行约束本质不同。1.3 废弃矿井小抽蓄一个被低估的灵活资源这里单独说下废弃矿井小型抽水蓄能。这个思路在国外已经有实际落地的探索本质是利用废弃矿井的竖井和巷道构建上、下两个蓄水池用矿井地形带来的自然落差作为水头。因为选址是“用现有的废弃空间”土地成本和开挖成本很低规模虽然做不到大型抽蓄那么夸张但胜在灵活。在建模上废弃矿井抽蓄要额外考虑几个现实约束上水库和下水库的库容都非常有限水位变幅不能太大水头随水位变化而变化严格来说不是恒定值抽水和发电的转换效率通常低于大型抽蓄还有矿井地质条件的限制比如巷道渗漏、岩体稳定性这些虽然不直接进入优化模型但在规划阶段决定了可用容量。我在模型中把它的上下水库分别用两个水量平衡方程来描述发电和抽水的状态是互斥的同一时刻只能处于其中一种工况并且用二进制变量表示启停状态。这套写法是标准的混合整数规划框架Python里用pulp或milk就能实现。2. 调度模型设计目标函数与关键约束2.1 目标函数怎么定才合理我做的是日前调度day-ahead scheduling时间尺度是24小时时间分辨率取1小时一个时段一共24个决策时段。在这个尺度下目标函数定义为$$ \min \sum_{t1}^{24} \left[ C_{buy,t}P_{buy,t} C_{curtail}(P_{wind,avail,t}-P_{wind,use,t} P_{pv,avail,t}-P_{pv,use,t}) C_{bat,degrade} \cdot |P_{bat,t}| C_{pump,start} \cdot y_{pump,t} \right] $$我来逐项解释一下这个公式的含义因为目标函数的每一项都直接影响模型的行为倾向。第一项C_{buy,t}P_{buy,t}是向上级电网购电的成本这个好理解。注意购电价格我用了分时电价把一天分成峰、平、谷三个时段价格参数在数据文件里直接给定。这一项的存在让模型天然倾向于在谷时多充电、峰时多放电。第二项是弃风弃光的惩罚项。P_{wind,avail,t}是t时段风电的可用出力也就是如果不弃风机能发多少P_{wind,use,t}是模型实际决定使用的风电出力两者差值就是弃风量。光伏同理。惩罚系数C_{curtail}设得很高比如500元/MWh这样只要电池或抽蓄还有一点点余量模型都不会选择弃掉风光的电。第三项是电池充放电的损耗惩罚。这里我用|P_{bat,t}|表示电池充放电功率的绝对值。为什么要加这一项因为电池充放电会折损循环寿命如果不在目标函数里体现这个代价模型会出现一种病态行为为了消纳一点点多余的风电让电池频繁小幅度充放毫无实际经济性。加入惩罚项后模型会权衡“多消纳的收益”和“电池损耗的成本”。第四项是抽蓄机组的启动惩罚项。y_{pump,t}是抽水工况的启动指示变量每次从非抽水状态切换到抽水状态y1。抽蓄机组频繁启停对设备寿命影响很大而且启动过程本身有能量损耗所以加一个固定的启动成本能有效减少无意义的频繁启停。2.2 功率平衡约束模型的心脏功率平衡是所有调度模型都必须满足的硬约束。对于并网型的风光储互补系统功率平衡方程是$$ P_{wind,use,t} P_{pv,use,t} P_{dis,t} P_{pumpgen,t} P_{buy,t} P_{load,t} P_{ch,t} P_{pumpload,t} $$这个式子从左到右读左边是“电源侧”包括实际使用的风电、光伏、电池放电、抽蓄发电、购电右边是“负荷侧”包括负荷、电池充电、抽蓄抽水消耗的电。两边每一时刻都要相等。这个约束的意义在于它把整个系统的源、荷、储通过调度决策串在了一起。模型做的所有优化决策本质上都是在为一个时段一个时段地满足这个等式。只要这一个约束写错或者写漏了整个模型就会出现功率不守恒的荒谬结果——比如系统发电量大于负荷加储能消耗多余的电凭空消失。写这个约束的时候有几个细节要特别注意电池的充电功率P_{ch,t}和放电功率P_{dis,t}不能同时为正这要通过二进制变量约束抽蓄的发电和抽水工况也是互斥的购电功率的上限由联络线容量决定。这些细节看似简单但恰恰是新手最容易忽略的地方。2.3 电池储能SOC连续变量加0-1变量电池模型的经典写法是三组约束。SOC递推方程$$ SOC_{t1} SOC_t \eta_{ch} \cdot P_{ch,t} \cdot \Delta t - \frac{P_{dis,t}}{\eta_{dis}} \cdot \Delta t $$η_ch是充电效率η_dis是放电效率Δt是时段长度1小时。我用的是一个简化模型把充放电效率当成常数处理。这样做的好处是模型保持线性求解速度快。如果需要更高精度可以用分段线性化的方法描述效率随SOC的变化但那是进阶玩法第一版模型没必要上。充放电互斥约束$$ P_{ch,t} \le P_{bat,rated} \cdot z_{ch,t}, \quad P_{dis,t} \le P_{bat,rated} \cdot z_{dis,t}, \quad z_{ch,t} z_{dis,t} \le 1 $$这里z_ch,t和z_dis,t是0-1变量确保电池不会一边充一边放。这是储能模型里的标配约束也是最容易写漏的。如果不加这个约束模型可能会利用“同时充放”这种物理上不可能的操作来钻目标函数的空子。SOC边界约束$$ SOC_{min} \le SOC_t \le SOC_{max}, \quad SOC_{24} SOC_0 $$末尾这个等式的意思是一天结束时SOC要回到初始值。这是日前调度的常见处理方式保证电池在每一天的运行里“有借有还”不会一天天把电量耗空或者充满。实际项目中如果你觉得这个约束太死板也可以改成SOC_24在初始值附近一定范围内比如±5%这取决于你的运行策略。2.4 抽蓄机组的模型比想象中复杂一点抽蓄的建模我建议把它拆成四个部分来写发电侧、抽水侧、上下水库水量平衡、以及工况互斥。发电功率约束$$ P_{pumpgen,t} \le P_{gen,rated} \cdot g_t $$这里g_t是发电工况的0-1状态变量。P_gen,rated是发电额定功率。抽水功率约束$$ P_{pumpload,t} \le P_{pump,rated} \cdot p_t $$p_t是抽水工况的状态变量。注意发电和抽水的额定功率可以不同实际设备里这两个功率通常是有差异的。上下水库水量平衡是抽蓄建模里最容易出错的地方。我之前在项目里踩过这个坑只建了一个水库的平衡方程结果模型在第二天上水库水干了、下水库却溢出来了纯物理不可能的事因为下水库的水量计算漏了。后来我把上下水库都写成平衡方程上水库$$ V_{up,t1} V_{up,t} \eta_{pump,hyd} \cdot P_{pumpload,t} \cdot \Delta t - \frac{P_{pumpgen,t}}{\eta_{gen,hyd}} \cdot \Delta t $$下水库$$ V_{low,t1} V_{low,t} - \eta_{pump,hyd} \cdot P_{pumpload,t} \cdot \Delta t \frac{P_{pumpgen,t}}{\eta_{gen,hyd}} \cdot \Delta t $$η_pump,hyd和η_gen,hyd是水泵工况和水轮机工况的水力效率。这两个方程一正一负本质上是水量守恒在两个库之间的体现。工况互斥约束$$ g_t p_t \le 1 $$这个约束表示同一时刻只能发电或者只能抽水不能同时进行。还需要额外约束g_t和p_t是二进制变量并且两个水库的库容上下限也要有否则模型会把库容用成无限大。3. Python实现从数学模型到可运行代码3.1 数据准备与参数初始化写代码之前我习惯把所有外部数据先整理成统一的CSV格式。调度模型的数据来源一般有四类风电预测出力、光伏预测出力、负荷预测曲线、分时电价。每类数据都是一列长度为24的序列对应24个时段。这些数据在真实项目中来自预测系统或历史统计在算例测试里来自典型日曲线或随机生成。我建议把这些数据读进一个DataFrame里这样可以很直观地看到数据有没有缺失、有没有明显异常。我的经验是在建模之前一定要花时间把数据质量检查一遍宁可多花半小时检查数据也不要让模型跑出来的结果因为数据问题而返工。数据读入之后把各类常数参数集中定义在模型文件的开头用全大写命名方便后续统一调整。import pandas as pd import numpy as np import pulp as pl # 读取预测数据 df pd.read_csv(data/forecast_day.csv, index_col0) load df[load].values wind_avail df[wind].values pv_avail df[pv].values price df[price].values # 系统参数 T 24 # 调度时段数 DT 1.0 # 时段长度(小时) P_BUY_MAX 80.0 # 外购电上限(MW) C_CURTAIL 500.0 # 弃风弃光惩罚(元/MWh) C_BAT_DEGRADE 25.0 # 电池损耗代价(元/MWh) # 电池参数 BAT_E_CH 0.95 # 充电效率 BAT_E_DIS 0.94 # 放电效率 BAT_CAP 40.0 # 额定容量(MWh) BAT_P_MAX 20.0 # 功率上限(MW) SOC_MIN 0.1 # SOC下限 SOC_MAX 0.9 # SOC上限 SOC_INIT 0.5 # 初始SOC # 抽蓄参数 PSH_V_UP_MAX 120.0 # 上水库容量(万m3简化单位) PSH_V_LOW_MAX 120.0 PSH_P_GEN_MAX 15.0 # 发电功率上限(MW) PSH_P_PUMP_MAX 12.0 # 抽水功率上限(MW) PSH_E_PUMP 0.80 # 抽水效率 PSH_E_GEN 0.85 # 发电效率 PSH_C_START 60.0 # 抽蓄启动成本(元/次)这套参数的数值设定不是随手写的。电池40MWh容量配20MW功率相当于2小时储能系统这是我常用的风电场配套储能比例的经验值。抽蓄15MW发电功率对应小型废弃矿井改造的典型水平再大的矿井改造项目也有但那个规模已经不是“小型”了。3.2 模型初始化与决策变量定义我用pulp库来建模因为它对混合整数线性规划支持好语法直观后端可以无缝切换GLPK、CBC、GUROBI等求解器。定义一个LP问题的标准写法是prob pl.LpProblem(WindPV_Storage_Complementary_Dispatch, pl.LpMinimize) # 决策变量 P_buy pl.LpVariable.dicts(P_buy, range(T), 0, P_BUY_MAX) P_wind pl.LpVariable.dicts(P_wind, range(T), 0) P_pv pl.LpVariable.dicts(P_pv, range(T), 0) P_ch pl.LpVariable.dicts(P_ch, range(T), 0, BAT_P_MAX) P_dis pl.LpVariable.dicts(P_dis, range(T), 0, BAT_P_MAX) SOC pl.LpVariable.dicts(SOC, range(T1), SOC_MIN, SOC_MAX) z_ch pl.LpVariable.dicts(z_ch, range(T), catBinary) z_dis pl.LpVariable.dicts(z_dis, range(T), catBinary) P_pumpgen pl.LpVariable.dicts(P_pumpgen, range(T), 0, PSH_P_GEN_MAX) P_pumpload pl.LpVariable.dicts(P_pumpload, range(T), 0, PSH_P_PUMP_MAX) V_up pl.LpVariable.dicts(V_up, range(T1), 0, PSH_V_UP_MAX) V_low pl.LpVariable.dicts(V_low, range(T1), 0, PSH_V_LOW_MAX) g pl.LpVariable.dicts(g, range(T), catBinary) p pl.LpVariable.dicts(p, range(T), catBinary) y_pump pl.LpVariable.dicts(y_pump, range(T), catBinary)注意SOC的索引范围是range(T1)因为SOC递推方程需要T和T1两个时刻的变量。我经常看到有新手在这里把SOC定义成range(T)结果递推方程索引越界或者最后一天的SOC没有明确约束求解出来的结果在末尾时段出现跳变。这类低级错误排查起来很费时间最好一开始就定义对。y_pump这个变量我需要单独说明一下。它是抽水工况的“启动”指示变量和p不一样。p表示“该时段是否处于抽水状态”y_pump表示“该时段是否刚刚启动抽水”。两者的关系是y_pump,t在且仅在p从0变为1时为1。在调度模型里启动变量表示的是一个跨时段的“状态切换”事件而不是稳态状态。它和约束的写法是for t in range(1, T): prob y_pump[t] p[t] - p[t-1]这个写法保证如果抽水状态发生从0到1的切换y_pump至少为1。因为目标函数里有C_PUMP_START * y_pump求最小化时y_pump会自动取最小值所以这个不等式约束就够了不需要再手动限定上限。这就是目标函数里那个启动成本项的用意——它让这个约束变成“软触发”而不是“强制相等”。3.3 目标函数与约束怎么写进求解器目标函数我用lpSum逐项累加。所有的大括号求和都可以直接用pl.lpSum(... for t in range(T))写出来非常直观。# 目标函数 prob ( pl.lpSum(price[t] * P_buy[t] for t in range(T)) C_CURTAIL * pl.lpSum((wind_avail[t] - P_wind[t]) (pv_avail[t] - P_pv[t]) for t in range(T)) C_BAT_DEGRADE * pl.lpSum(P_ch[t] P_dis[t] for t in range(T)) PSH_C_START * pl.lpSum(y_pump[t] for t in range(T)) )这里关于电池损耗项我用的是P_ch[t] P_dis[t]而不是绝对值abs(P_bat[t])。因为P_ch和P_dis都被限制在非负范围内两者之和实际上就是电池充放电功率的绝对值。这样写既规避了绝对值函数的非线性又准确表达了损耗代价是MILP建模里的一个小技巧。Pulp的线性求解器不支持abs()所以遇到绝对值项第一反应要想到用“非负变量拆分”的方式来处理。功率平衡约束如下。漏掉任何一项都会导致整个调度结果失真我一般写完会对着公式逐项核对一遍for t in range(T): prob (P_wind[t] P_pv[t] P_dis[t] P_pumpgen[t] P_buy[t] load[t] P_ch[t] P_pumpload[t])电池相关约束prob SOC[0] SOC_INIT for t in range(T): prob SOC[t1] SOC[t] BAT_E_CH * P_ch[t] * DT - P_dis[t] / BAT_E_DIS * DT prob P_ch[t] BAT_P_MAX * z_ch[t] prob P_dis[t] BAT_P_MAX * z_dis[t] prob z_ch[t] z_dis[t] 1 prob SOC[T] SOC_INIT抽蓄相关约束。水量平衡方程里的单位需要说明一下P_pumpload是电功率MW乘以效率后代表实际把水抽上去的“水力功率”再乘以DT小时就是抽上去的水量我这里简化了单位实际项目需要换算成立方米乘一个水头和重力加速度相关的系数。这里的写法重在演示逻辑具体单位换算要看项目的数据手册prob V_up[0] PSH_V_UP_MAX * 0.5 prob V_low[0] PSH_V_LOW_MAX * 0.5 for t in range(T): # 上水库水量平衡 prob (V_up[t1] V_up[t] PSH_E_PUMP * P_pumpload[t] * DT - P_pumpgen[t] / PSH_E_GEN * DT) # 下水库水量平衡 prob (V_low[t1] V_low[t] - PSH_E_PUMP * P_pumpload[t] * DT P_pumpgen[t] / PSH_E_GEN * DT) # 工况互斥 prob g[t] p[t] 1 prob P_pumpgen[t] PSH_P_GEN_MAX * g[t] prob P_pumpload[t] PSH_P_PUMP_MAX * p[t] # 启动标记 if t 0: prob y_pump[t] p[t] - p[t-1] else: prob y_pump[t] p[t]我在写抽蓄的初值V_up[0] PSH_V_UP_MAX * 0.5时是让上水库初始水位为50%。这个初值选择会影响当天的调度自由度如果初始水位太低白天可能蓄不了足够的水供晚高峰发电如果太高上午没有抽水空间光伏大发时没法把多余的电存进水库。实际项目中这个初始水位应该由前一天的调度结果决定属于“跨日耦合”的问题。3.4 求解器选择CBC最容易上手但商业化求解器更快求解橙色可以直接调用prob.solve()默认使用CBC求解器。CBC是开源求解器对中小规模MILP问题完全够用。我自己测试过一个24时段的模型决策变量大概有200多个其中二进制变量70多个CBC求解时间通常在1~3秒内完成非常快。但有个问题需要注意如果模型规模扩大比如扩展到96个时段15分钟分辨率、多台机组、多种储能并联CBC的速度会明显下降。我在一个扩展算例里把时段数改成96模型规模扩大到上千个变量CBC跑了将近两分钟才收敛。这种时候我会切换成商业求解器比如GUROBI或CPLEX。Pulp的支持方式很简单prob.solve(pl.GUROBI_CMD())或者prob.solve(pl.CPLEX_CMD())如果机器上装了对应的求解器直接改这一行就能切换。我建议如果条件允许至少在最终算例阶段用GUROBI跑一遍因为商业求解器的数值稳定性更好对病态约束的容忍度更高排错时更容易判断是模型问题还是求解器问题。4. 算例分析与参数调优4.1 对比实验怎么设计才有效代码跑通之后最重要的就是设计对比实验来验证模型的有效性。我在这个项目里设计了三个对比场景场景A是“风光电池抽蓄全上”场景B是“只有电池没有抽蓄”场景C是“只有抽蓄没有电池”。三个场景用同一套风光和负荷数据跑出来的结果差异一眼就能看出储能互补的价值。场景B没有抽蓄时电池需要单独扛起全天的调峰责任。结果是电池的充放电深度明显增大某些时段SOC直接顶到上限或者放到下限。虽然弃风弃光率也能压下来但电池损耗项明显增高。场景C没有电池时晚高峰负荷突增的阶段抽蓄启动需要时间响应不够快在光伏突然跌落的那几个小时里不得不大量购电。只有场景A电池负责快速响应、抽蓄负责跨时段搬移两者配合购电成本和弃风弃光率都是最低的。这种对比实验是论文里的标配但对工程实践也有指导意义储能配比不是越贵越好不同类型的储能搭配使用在技术和经济上都有优势。4.2 几个关键参数对结果的影响我在调参过程中发现参数对结果影响的几个规律供参考弃风弃光惩罚系数 C_CURTAIL这个系数设得太小比如10元/MWh模型宁可弃掉风光也不愿多充几次电设得太大模型会为了消纳最后一兆瓦的风电让电池和抽蓄做极度不经济的操作。我调试下来的比较合理的区间是100元/MWh到500元/MWh之间具体取决于当地的上网电价和绿电交易价格水平。电池损耗代价 C_BAT_DEGRADE这个参数如果设成0模型会在每个时段都让电池有微小的充放动作结果就是SOC曲线像锯齿一样高频抖动。设成25~40元/MWh后SOC曲线变得平滑很多电池的每日有效循环次数也降到了合理范围。这里可以参考锂电池度电成本的经验值来设定。抽蓄启动成本 C_PUMP_START没有这个参数时抽蓄机组可能在一个小时内切换好几次工况在约束允许的范围内反复抽水、发电、再抽水。设置启动成本后模型自动把抽水时段聚合成连续的单次抽水实际运行操作性好很多。SOC的初始值和终值SOC_INIT和SOC_END如果设置不合理比如初始50%但终值强制100%相当于免费给第二天多存了电第一天的运行成本会虚低但这个收益是透支第二天的。我在做跨日仿真时会把SOC_INIT和SOC_END都设为50%然后多跑几天看每天的起始状态是否能够自然衔接。4.3 结果可视化的几个关键图调度结果出来后我通常画四张图功率平衡堆叠图、SOC曲线图、抽蓄水库水位变化图、弃风弃光率柱状图。功率平衡堆叠图是最关键的一张x轴是24小时y轴是功率负载曲线用黑色实线电源侧用不同的颜色堆叠。这张图能一眼看出是否有功率不守恒的地方也能看出光伏大发时段风电光伏的消纳情况。SOC曲线图主要用来检查电池行为是否合理——如果SOC曲线频繁触及上下限说明储能容量和系统需求不匹配需要调整电池容量或者充放电策略。水库水位图用来验证抽蓄的水量平衡是否合理正常情况两条曲线应该关于某条水平线镜像对称。5. 常见问题与排查经验5.1 求解结果“不收敛”或“无解”怎么办先说一下现象Pulp求解器报Status: Infeasible意思是约束互相矛盾模型找不到任何可行解。我的排查经验是按顺序执行下面几个步骤第一步检查电池SOC边界和初始终值。如果SOC_INIT是0.9SOC_END是0.1而电池容量很小一天内根本放不出那么多电量模型就会无解。第二步检查功率平衡约束两边单位是否一致。我遇到过把MWh和MW混用了结果约束不等式错得离谱。第三步逐个注释掉约束试跑确认是哪一组约束导致的无解。这个方法效率最高通常十几分钟就能定位问题。第四步检查抽蓄上下水库的容量是否足够容纳一天内的水量转移。如果上水库容量只有50但一天内需要搬移的水量是200无解是必然的。5.2 储能SOC曲线“锯齿状”抖动是为什么这个问题我在初版模型里遇到过SOC曲线在相邻时段来回剧烈波动每15分钟就大幅充放一次。原因就是前面说的目标函数里没有电池损耗代价项。没有损耗惩罚时模型利用电池的“免费”特性做无意义的电量搬移因为它在数值上确实能降低一点点购电成本。解决方案就是加C_BAT_DEGRADE这一项给电池的每次充放动作标一个合理的价格。加上之后SOC曲线变得平滑电池一天的有效循环次数也降到了合理范围。这个参数是储能调度模型里最容易调出效果的一个也是我强烈建议你在初始建模时就加上的。5.3 抽蓄机组频繁启停怎么抑制如果你发现抽蓄的工况在一小时内反复切换从抽水切发电再立即切回抽水目标函数里也没有启动成本那大概率是缺少启动变量的约束或者是启动成本的数值太小不足以约束启停行为。把启动成本调到比较明显的量级比如50~100元/次加上启动变量和p[t]的前后差分约束问题就解决了。另一个容易被忽略的细节是如果抽蓄的抽水功率上限小于发电功率上限模型的功率平衡约束里抽水侧消耗少、发电侧供给多会让模型更愿意选择发电工况。这本身是合理的物理规律但在参数设置时要确保两个上限和实际设备铭牌一致不能随意填。5.4 代码层面的几个坑最后整理几个我在代码实现中踩过的小坑。Pulp的变量名重复会导致求解异常。如果你在两个循环里不小心用了同一个变量名Pulp会直接报变量重复定义的错误。我的做法是所有的dict变量名都带有明确的后缀且每个变量都在模型文件开头集中定义方便排查。数据类型问题。从DataFrame读出来的数据如果是int型但在运算中需要float有时会悄悄影响数值结果。建议在读入数据后统一.astype(float)。P_Buy、P_wind、P_pv这些变量的上下限设置。如果我忘了给P_wind设置上限它应该不超过P_wind_avail模型可能会在弃风惩罚系数大于电价差时选择“虚拟发电”。实际上P_wind的上限约束要写清楚for t in range(T): prob P_wind[t] wind_avail[t] prob P_pv[t] pv_avail[t]这两条约束一定要加上它表示实际使用的风光出力不可能超过当时的可用出力。这个看似“理所当然”的约束漏掉了整模型就会产生根本性的逻辑错误。还有一个在多时段建模中容易犯的错误Pulp里添加约束的顺序会影响求解器内部的模型结构但不影响数学结果。如果你在约束还没定义完全的时候就调用solve()它会基于“当前的模型状态”求解结果可能少约束。所以我习惯的做法是把所有的约束全部添加完之后再调用solve()并且求解前先打印一下模型规模看一眼。我对这套“风光电池废弃矿井抽蓄”互补调度框架的整体感受是模型本身不复杂做难的是让模型结果贴近物理实际。目标函数每一项都对应一个真实的运行代价约束每一条都对应一条物理规律。把这些理清楚Python代码就只是把这套逻辑逐字翻译成pulp语法而已。按照我上面的流程从数据准备到出结果一个24时段的日前调度模型大概一天之内就能跑通。后续要扩展可以考虑在模型里加上不确定性用场景法或鲁棒优化处理风光预测误差或者把时间分辨率从1小时细化到15分钟模型框架都可以平滑升级。跑数据的时候留意一下异常时段如果某个时段的调度结果明显偏离直觉不要急着改代码先看看原始数据在那个时段有没有异常值——这个习惯能帮你少走很多弯路。
返回列表