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

Python实现微电网日前经济调度:风光储能与需求响应建模

Python实现微电网日前经济调度:风光储能与需求响应建模 ★ FEATURED ARTICLE
1. 从调度问题说起这到底是在算什么做微电网的同学应该都有同感刚接触“日前经济调度”这几个字的时候容易把它理解成一个简单的“负荷预测 机组组合”问题。其实真上手做一遍就会发现它本质上是在不确定性中做确定性决策风电、光伏的出力是波动的负荷是变化的储能是双向的需求响应又把用户侧的弹性拉了进来。所有因素叠加在一起你要在一天开始之前给出未来 24 小时每一台设备、每一个时段的最优运行计划并且确保这个计划在经济上最省、在物理上可行。这篇项目的核心就是用 Python 实现一套基于风光储能和需求响应的微电网日前经济调度模型。它解决的典型场景是这样某个微电网里装了风力发电、光伏板、一组蓄电池储能还有一部分可平移、可削减的柔性负荷。现在要给明天的 24 个时段分别制定储能什么时候充电、什么时候放电、充放多少功率需求响应在哪些时段启动、削减多少负荷以及从主网购电的计划是多少。 目标是在满足所有负荷需求的前提下让总运行成本最低。这类问题在学术论文里很常见但真要复现出来牵涉到的东西比想象中多得多。首先是风光出力的预测曲线怎么处理其次是储能的充放电状态怎么用整数变量表达再就是需求响应要不要拆成连续变量和整数变量组合建模最后还要用线性规划求解器把这些约束全部喂进去。任何一个环节建模不准确求解结果就可能在物理上不可行——比如储能同时充放电或者需求响应把负荷削减成了负数。我最早接触这个题目的时候是帮一个做园区能源管理的朋友搭原型。他们的真实痛点是园区里光伏装机不小但午间光伏大发的时候负荷反而低电送不出去傍晚负荷上来了光伏又没了。如果没有储能和需求响应只能眼睁睁看着高峰时段从电网高价买电。所以这套模型的价值不只是算一个最优解出来而是能告诉你储能该在什么时候“囤电”、需求响应该在什么时段“削峰”这些决策直接对应真金白银的成本节省。这篇文章我不打算只贴一段能跑的代码而是把建模思路、数学形式、Python 实现细节、踩过的坑一起讲清楚。适合三类人看一是正在写微电网方向论文、需要复现调度模型的研究生二是做园区或楼宇能量管理系统、需要落地日前调度策略的工程师三是想学习用混合整数线性规划解决实际工程问题的 Python 用户。后面的内容我会尽量按“从问题到代码”的顺序展开涉及到公式的地方也会用比较直白的方式解释避免一上来就被符号劝退。2. 模型设计变量、目标函数和约束怎么搭2.1 时段划分和基础数据准备日前调度的第一件事是把一天拆成离散时段。常规做法是取 24 个时段每个时段 1 小时。有些项目为了提高精度会取 96 个时段每 15 分钟一个点但那样变量数量和求解时间都会翻几倍。我自己的经验是如果只是为了验证调度策略的合理性24 时段完全够用如果要发布正式结果或者对接实际控制系统再考虑 15 分钟粒度。每个时段需要准备的基础数据包括负荷预测值kW风电出力预测值kW光伏出力预测值kW分时电价元/kWh需求响应参数可削减比例、补偿价格等这里面有个容易忽略的点风光出力的预测值应该是“有功功率上限”而不是固定出力。什么意思呢就是说在调度模型里风电机组和光伏逆变器在某个时段的实际出力是一个决策变量预测曲线只是给了一个允许出力的上限。如果电网消纳不了或者储能已经充满、负荷也很低调度器可以选择弃风弃光而不是傻乎乎地按预测值全额出力。这一点我在初版建模时吃过亏直接把预测出力当成固定值塞进平衡约束结果求解出来的方案里储能拼命充电、负荷端还需要购电实际跑下来根本不合理。2.2 风光出力的建模方式风电和光伏在日前调度模型里通常被简化成功率上限约束数学上写成[ 0 \le P_{wt}(t) \le P_{wt}^{fore}(t) ] [ 0 \le P_{pv}(t) \le P_{pv}^{fore}(t) ]注意这是个不等式不是等式。它的意思是“你最多能发这么多但实际发多少由优化决策说了算”。为什么要这样设计因为从经济调度的角度看风光机组在边际成本上是零所以目标函数里没有它们的发电成本项。既然成本为零优化器会倾向于尽量多发——但前提是系统能消纳。如果某个时段负荷很低、储能充满、又不允许向电网倒送电那电网平衡约束就会逼着模型把某个电源的出力压下来。这时候优先压谁因为风光零成本理论上应该压购电或者压储能充电但实际上购电可能已经为零、储能充电功率有上限那就只能压风光出力。这个逻辑在模型里是靠不等式约束自动实现的不需要人为干预。光伏模型还可以做得更细一点比如加一个与光照强度相关的效率系数或者考虑温度对组件效率的影响。但在日前调度这个时间尺度上这些细节对结果的影响远小于预测误差本身所以更常见的做法是把预测曲线直接当上限简化处理。风电也一样不需要模拟功率曲线直接用预测功率作为上限即可。这种简化不会让模型失真反而让求解过程更稳定。2.3 储能系统建模充电、放电和状态约束储能是微电网调度里最容易“翻车”的建模对象因为它的核心特征在于状态连续性——前一时刻的荷电状态会直接影响下一时刻。一个标准的储能模型包含三个部分SOC 递推方程、充放电功率约束、以及避免同时充放电的整数约束。SOC 递推方程写出来是这样[ SOC(t) SOC(t-1) \eta_{ch} \cdot P_{ch}(t) \cdot \Delta t - \frac{P_{dis}(t)}{\eta_{dis}} \cdot \Delta t ]其中 (\eta_{ch}) 和 (\eta_{dis}) 分别是充电和放电效率(\Delta t) 是时段长度小时。注意放电项的写法它是把放电功率除以放电效率而不是乘以效率因为从电池内部角度看要放出 (P_{dis}) 的交流侧功率实际消耗的电池能量是 (P_{dis}/\eta_{dis})。这个方向一旦搞反整个 SOC 曲线就会失真。充放电功率约束要考虑到电池的额定功率限制[ 0 \le P_{ch}(t) \le P_{ch}^{max} \cdot u_{ch}(t) ] [ 0 \le P_{dis}(t) \le P_{dis}^{max} \cdot u_{dis}(t) ] [ u_{ch}(t) u_{dis}(t) \le 1 ](u_{ch}(t)) 和 (u_{dis}(t)) 是 0-1 整数变量用来禁止储能同时充电和放电。这个约束必须有否则线性规划求解器会把储能当成“合法的搬运工”——一边充电一边放电两边都产生功率但净功率为零不仅没有任何实际意义还会污染成本计算。SOC 本身也需要约束上下限[ SOC^{min} \le SOC(t) \le SOC^{max} ]为了延长电池寿命一般不会让 SOC 跑到 0% 或 100%。实际工程中常见的区间是 20% 到 90%甚至更保守。还有一个约定俗成的处理调度周期开始和结束时的 SOC 要相等或者至少结束时要回到一个指定值。这么做是为了保证储能的“日循环”独立性——你今天把电放光了明天早上就没有电可用了这种方案虽然在单日模型里成本最低但对连续运行没有参考意义。2.4 需求响应模型的三种常见形式需求响应是这类模型里最灵活、也最容易被过度复杂化的部分。按工程实践来看有三种常见的建模形式。第一种是可削减负荷interruptible load。这种负荷可以在特定时段被切掉一部分但削减量有上限而且用户会获得补偿。数学上就是给负荷平衡约束加一个削减变量并在目标函数里加一个补偿成本项。第二种是可平移负荷shiftable load。洗衣机、洗碗机这类负荷可以从高峰时段挪到低谷时段但总量不变。这种需要引入“平移前后状态”的对应关系通常用整数变量表达时段分配复杂度和变量数量都会明显增加。第三种是价格型需求响应也就是根据电价弹性调整负荷水平。这种一般用价格弹性矩阵建模写起来最简洁但参数不好标定实际应用场景相对较少。在这个项目里我选择的是第一种和第三种的结合基础负荷不可动但允许一定比例的负荷在高峰时段被削减。削减量作为连续变量单位削减成本设定为低于高峰购电电价。这样优化器在高峰时段会权衡是高价买电、还是花钱请用户少用电。只要补偿价格设置合理模型会自动在尖峰时段触发需求响应效果非常直观。这种设置的好处在于它让模型能反映需求响应的“自愿性”——削减不是强制执行的而是基于经济比较的自动决策。你不需要人为指定“18 点必须削减 100kW”只需要把补偿价格和削减上限给定优化器自然会算出一个最优的削减计划。这也是需求响应最打动工程人员的一点它把用户侧的弹性变成了可以参与优化的资源。2.5 与主网交互的建模微电网不是孤岛它还要跟上级电网交易。常规做法是引入两个非负变量购电功率 (P_{buy}(t)) 和售电功率 (P_{sell}(t))。这两者也不能同时为正所以需要再加一组整数约束[ P_{buy}(t) \le P_{buy}^{max} \cdot u_{buy}(t) ] [ P_{sell}(t) \le P_{sell}^{max} \cdot u_{sell}(t) ] [ u_{buy}(t) u_{sell}(t) \le 1 ]购电成本按分时电价计算售电收益按上网电价计算。因为购电价一般高于上网电价所以模型天然倾向于“低谷买电、高峰自给自足”这符合实际运行逻辑。但要注意如果允许售电且光伏大发时段电价又高模型可能会倾向于先把电卖给电网这个行为在真实场景里受并网协议限制所以在模型里要给售电功率设上限不能让它变成无约束的套利工具。到这里模型的核心组件就齐了风光机组、储能、需求响应、与主网的交互。剩下的问题是怎么把这些组件拼成一个优化问题写出可求解的目标函数和约束条件。3. 目标函数与约束条件的具体数学形式3.1 目标函数日运行总成本最小化目标函数是所有建模工作的“心脏”。在日前经济调度里我们最小化的是未来 24 小时的运行总成本它由以下几部分组成购电成本每个时段从主网购电的费用等于购电功率乘以该时段电价再把 24 个时段加起来。需求响应补偿成本削减负荷需要给用户补偿等于削减量乘以单位补偿价格。储能退化成本可选项目。如果考虑电池循环寿命损耗可以给充放电加一个成本系数。学术界常用一个固定成本系数乘充放电电量来近似工程上也有用 SOC 波动惩罚的变体。弃风弃光惩罚可选项目。风光零成本但如果你希望调度结果尽量多用清洁能源可以给弃风弃光加一个很小的惩罚系数防止模型毫无理由地压减风光出力。公式表达就是[ \min \sum_{t1}^{24} \left[ c_{buy}(t) \cdot P_{buy}(t) c_{dr}(t) \cdot P_{cut}(t) c_{ess} \cdot (P_{ch}(t) P_{dis}(t)) \right] ]这个目标函数设计成线性形式是刻意的。如果目标函数里有非线性项比如储能退化成本写成 SOC 的二次函数整个问题就从线性规划变成了非线性规划求解难度陡增。工程上能用线性近似就尽量用线性近似这是我做优化模型的一条基本准则。3.2 关键约束条件分组解析约束条件按物理对象分组最核心的有几组我逐个说一下。功率平衡约束——这是整个模型的骨架[ P_{wt}(t) P_{pv}(t) P_{dis}(t) P_{buy}(t) P_{load}(t) - P_{cut}(t) P_{ch}(t) P_{sell}(t) ]这个式子的含义很朴素所有电源出力加上储能放电加上购电必须等于负荷减去需求响应削减量再加上储能充电和售电。这里我把需求响应处理成“减少负荷”而不是“增加电源”因为它本质上是用户侧的行为。注意储能充电放在了等式右边作为负荷放电放在左边作为电源这样符号关系一目了然。在实际编码时这条约束最难调试。因为它把几乎所有变量都牵扯进来了任何一个变量的上下界设置有误都会在这个等式上爆出不可行解。我调试的时候习惯把每个时段的风光负荷数据先单独拿出来检查一遍确认“可发电量 可购电量 储能可放量”是否真的能覆盖“负荷 储能充电需求”。如果原始数据本身就不满足这个条件那模型必然无解——不是算法的错是数据的问题。储能系统约束——前面已经列过这里要补充的是 SOC 初始值设定。我一般取 SOC(0) 50%并要求调度结束时刻 SOC(24) 50%。这个不是死规定但工程上默认这样做防止模型把储能能量“吃到亏空”。如果你想强化储能的日循环属性可以改成 SOC(24) SOC(0)。风光出力约束——即每个时段的实际出力不超过预测上限而且不能为负。这是最自然的约束没有任何容易踩坑的地方但它是连通预测数据和优化模型的关键桥梁。需求响应约束——削减量不能超过该时段原始负荷的一定比例。我通常取 10%~20%具体取决于用户的用电特性。如果设置太高就等于允许模型随便切负荷虽然成本降下来了但实际执行时用户会强烈反弹。与主网交易约束——购电和售电不能同时进行。前面已经给出整数约束形式。备用容量约束——可选但推荐加入。微电网里有不确定性所以通常要求系统在每个时段留有至少 5%~10% 的旋转备用容量。这个约束可以写成储能和主网购电的剩余可调能力之和大于备用需求。加了它模型会更贴近实际但求解时间也会相应增加。3.3 为什么选混合整数线性规划而不是启发式算法每次讲到这里都会有人问为什么不直接用遗传算法或者粒子群算法这个问题要从问题结构说起。微电网日前经济调度里储能的充放电状态、购售电状态都是离散决策需要用 0-1 整数变量表达而功率大小、SOC 变化都是连续决策。整个模型天然是一个混合整数线性规划MILP问题。MILP 的好处在于它有一套成熟的数学理论保证全局最优解求解器可以告诉你“这个解是不是最优的”以及“离最优解还差多少”。相比之下启发式算法虽然编写门槛低但它不保证找到全局最优解甚至两次运行的结果可能都不一样。当然MILP 的劣势也很明显当整数变量数量膨胀到几千个时求解时间会显著上升。但在 24 时段 × 每时段几个整数变量的规模下主流求解器通常几秒到几十秒就能出结果完全在可接受范围内。所以我从一开始就坚定选择 MILP 路线没有考虑启发式算法。后者更适合变量规模巨大、精确求解不可行的场景但那个场景在日前调度这个小尺度问题里并不存在。4. Python 代码实现从建模到求解4.1 工具选型用哪个求解器Python 生态里做 MILP 的库不少常见的有PuLP上手简单语法接近自然语言适合教学和原型验证MIPPython-MIP封装了商业求解器的接口性能不错scipy.optimize.milpSciPy 1.9 之后自带 MILP 接口免安装额外依赖但功能相对基础OR-ToolsGoogle 出品功能强大适合更大规模的问题Gurobi / CPLEX商业求解器性能最优但需要授权我个人的建议是如果你只是复现算法、验证模型逻辑用 PuLP 或者 Python-MIP 就够如果你要对接生产系统、跑大规模算例再考虑 Gurobi。这篇项目里我用的是 Python-MIP因为它的语法比 PuLP 更紧凑对可变下标变量的支持也更自然求解速度在中小规模问题上跟商业求解器差距不大。建模之前先做一次数据清洗。把风电、光伏、负荷三条曲线画在同一张图里肉眼检查有没有缺失值和异常跳变。这一步看着土但能省下后面大量调试时间。我自己就遇到过光伏数据在中午时段突然出现 0 值的情况查了半天发现是原始数据里的小数点错误如果不画图根本发现不了。4.2 核心建模代码框架下面是一段核心建模代码展示了从参数定义到约束添加的完整流程。在实际项目中数据读取部分可以用 pandas 完成这里我重点展示模型构建部分。from mip import Model, xsum, maximize, minimize, BINARY, CONTINUOUS, INTEGER # 基本参数 T 24 # 时段数 dt 1.0 # 时段长度小时 # 预测数据示例值实际从文件读取 load_forecast [120, 115, 110, ...] # 24个时段的负荷预测 wind_forecast [30, 28, 25, ...] # 风电出力上限 pv_forecast [0, 0, 0, ...] # 光伏出力上限 price_buy [0.45, 0.42, 0.38, ...] # 分时购电价 # 储能参数 ess_capacity 200 # kWh ess_power 50 # kW soc_min, soc_max 0.2, 0.9 soc_init 0.5 eta_ch, eta_dis 0.95, 0.95 # 需求响应参数 dr_ratio 0.15 # 最大可削减比例 dr_price 0.8 # 削减补偿单价 # 创建模型 m Model(microgrid_dispatch, senseminimize) # 决策变量 p_wt [m.add_var(namefp_wt_{t}, lb0, ubwind_forecast[t]) for t in range(T)] p_pv [m.add_var(namefp_pv_{t}, lb0, ubpv_forecast[t]) for t in range(T)] p_ch [m.add_var(namefp_ch_{t}, lb0, ubess_power) for t in range(T)] p_dis [m.add_var(namefp_dis_{t}, lb0, ubess_power) for t in range(T)] p_buy [m.add_var(namefp_buy_{t}, lb0) for t in range(T)] p_sell [m.add_var(namefp_sell_{t}, lb0) for t in range(T)] p_cut [m.add_var(namefp_cut_{t}, lb0) for t in range(T)] u_ch [m.add_var(namefu_ch_{t}, var_typeBINARY) for t in range(T)] u_dis [m.add_var(namefu_dis_{t}, var_typeBINARY) for t in range(T)] u_buy [m.add_var(namefu_buy_{t}, var_typeBINARY) for t in range(T)] u_sell [m.add_var(namefu_sell_{t}, var_typeBINARY) for t in range(T)] soc [m.add_var(namefsoc_{t}, lbsoc_min, ubsoc_max) for t in range(T)] # 目标函数 m.objective xsum( price_buy[t] * p_buy[t] dr_price * p_cut[t] for t in range(T) ) # 可选的储能退化成本 # ess_degrade_coef * (p_ch[t] p_dis[t]) # 功率平衡约束 for t in range(T): m p_wt[t] p_pv[t] p_dis[t] p_buy[t] \ load_forecast[t] - p_cut[t] p_ch[t] p_sell[t] # 储能SOC递推 for t in range(T): if t 0: m soc[t] soc_init eta_ch * p_ch[t] * dt - p_dis[t] / eta_dis * dt else: m soc[t] soc[t-1] eta_ch * p_ch[t] * dt - p_dis[t] / eta_dis * dt # 储能同时充放电约束 for t in range(T): m u_ch[t] u_dis[t] 1 m p_ch[t] ess_power * u_ch[t] m p_dis[t] ess_power * u_dis[t] # 购售电互斥约束 for t in range(T): m u_buy[t] u_sell[t] 1 m p_buy[t] 300 * u_buy[t] # 300kW为购电功率上限 m p_sell[t] 100 * u_sell[t] # 100kW为售电功率上限 # 需求响应削减上限 for t in range(T): m p_cut[t] dr_ratio * load_forecast[t] # 调度结束SOC恢复约束 m soc[T-1] soc_init # 求解 m.optimize() # 结果提取 if m.status.value 0: # 0表示最优 for t in range(T): print(f时段{t1}: 风电{p_wt[t].x:.1f}kW, 光伏{p_pv[t].x:.1f}kW, f充电{p_ch[t].x:.1f}kW, 放电{p_dis[t].x:.1f}kW, f购电{p_buy[t].x:.1f}kW, 削减{p_cut[t].x:.1f}kW, fSOC{soc[t].x:.2f})4.3 代码设计里的几个关键决策代码看起来不长但里面有几个决策直接决定模型能不能跑出合理结果。第一个决策是上限值的硬编码。在购电互斥约束里我写的是p_buy[t] 300 * u_buy[t]这 300 是从主网联络线的容量限制推出来的。如果你跑自己的数据一定要根据实际情况改这个值不能照抄。如果上限设得太大整数变量的松弛空间就大求解器需要更多时间探索如果设得太小可能把可行解空间砍掉导致无解或次优解。第二个决策是SOC 递推公式放在约束而不是脚本里。很多初学者喜欢在 Python 里用循环算 SOC再把结果作为常数传给模型。这等于把 SOC 序列“锁死”了储能完全丧失了调度灵活性。正确的做法一定是把 SOC 作为决策变量把递推关系写成等式约束让求解器自己决定每个时段的充放电量。这个区别是整个储能调度模型成立的基础。第三个决策是时间段从 0 开始编号还是从 1 开始。这个看似无关紧要但在写 SOC 递推时会很痛苦。我统一采用 0 到 23 的 Python 编号循环里用if t 0处理初始条件代码逻辑非常干净。如果你的数据文件里时段编号是 1 到 24建议在数据预处理阶段就转成 0 基不要让建模代码和业务数据的编号方式纠缠在一起。4.4 求解结果怎么解读跑完求解器之后不要只看目标函数值就完事一定要做结果合理性检查。检查顺序我建议是这样的第一SOC 曲线是否平滑。如果 SOC 曲线出现剧烈的锯齿状波动——一会儿从 20% 冲到 90%一会儿又从 90% 掉回 20%——那很可能说明目标函数里缺少对储能充放电行为的惩罚项或者数据有问题。储能在真实运行中不会这样频繁深度充放。第二有没有同时充放电。虽然加了整数约束禁止同时充放电但有些求解器在容差范围内可能给出极端接近 0 的数值比如充电 0.001kW、放电 0.0005kW。这时候不用纠结直接把小于 0.01 的值当 0 处理即可不影响实际结果。第三需求响应集中在哪些时段。正常情况下削减应该发生在电价最高的几个时段。如果削减发生在低谷时段说明你的补偿价格模型出了问题或者约束写错了。这一点是最直观的“模型自检信号”。5. 结果分析与参数影响5.1 典型调度结果长什么样我拿一组典型数据跑过一次负荷曲线是典型的双峰型——早高峰出现在 9 到 11 点晚高峰出现在 18 到 21 点。光伏出力集中在 10 点到 15 点风电出力在夜间较大。最优方案表现出的行为是夜间到凌晨23 点到 5 点风电出力充足负荷较低。储能从 50% SOC 开始充电到 5 点左右充到 80%。这时候购电电价最低偶尔也会从主网买一点电给储能充电——这是套利行为完全合理。上午时段7 点到 10 点电价开始升高储能停止充电逐步转为放电。如果光伏出力还没上来就靠储能顶着早高峰。中午时段11 点到 14 点光伏大发负荷相对平坦储能基本不充不放处于“待命”状态。如果光伏超出负荷和充电需求考虑小幅售电。傍晚时段18 点到 21 点光伏归零负荷达到全天最高储能开始大规模放电。此时需求响应被触发削减了约 10% 的晚高峰负荷。购电也达到全天上限。晚 22 点负荷回落储能 SOC 已经掉到接近 20%但调度结束要求回到 50%所以会有一小段充电过程。这套行为模式完全符合经济直觉低谷充电、高峰放电、尖峰削减、风光全消纳。看到这个结果基本可以断定模型没有大的逻辑错误。5.2 需求响应补偿价格的影响力需求响应在模型里是最有意思的参数因为补偿价格直接决定了模型“愿不愿意”削减负荷。当补偿价格远低于高峰购电电价时优化器会优先削减负荷当补偿价格接近甚至高于购电电价时削减就显得不划算模型会转而购电或者用储能放电。我做了一组分段测试补偿价格从 0.2 元/kWh 一路加到 1.2 元/kWh观察削减量和总成本的变化。结果是价格在 0.5 元以下时削减量几乎没有变化——因为这时候高峰电价1.0 元以上远高于补偿价格削减的性价比太高了不用白不用价格超过 0.8 元后削减量开始明显回落而到了 1.2 元削减量已经比最高峰时下降了六成以上。这个测试给工程带来的启示是需求响应补偿价格的设定不用太激进只要明显低于高峰电价就能充分发挥它的削峰价值。如果追求更精细的调节可以把削减量也做成阶梯式——比如前 5% 的削减补偿 0.5 元后 5% 补偿 0.8 元——这种阶梯定价在建模上就是把一个连续变量拆成多段线性变量在线性规划的框架里完全可行。5.3 储能容量对总成本的影响储能容量也是值得做灵敏度分析的对象。我把储能容量从 50 kWh 一直加到 500 kWh每次跑一遍模型记录总成本。结果是一条典型的“边际递减”曲线容量在 50 到 150 kWh 这个区间总成本下降非常明显200 kWh 以后边际效果逐渐减弱当容量超过 350 kWh总成本几乎不再变化——因为此时储能已经能完全覆盖早晚两个高峰的转移需求再大的容量在单日调度周期内没有“用武之地”。这个结果对实际项目选型有很强的参考价值。很多人直觉上认为“储能越大越好”但日前调度模型告诉我们在给定负荷曲线和电价结构下存在一个经济上最优的储能容量区间。超过这个区间多出来的容量只有初始投资没有运行收益反而会增加电池更换的成本。把这些灵敏度分析结果做成曲线放进可研报告说服力比单纯说“需要装储能”强得多。6. 常见问题与排查技巧实录6.1 求解器报“不可行”怎么办这是所有优化模型都会遇到的问题也是最考验耐心的地方。我总结出一套排查套路按顺序执行几乎都能解决问题。第一步检查数据一致性。把每个时段的“总可供应量”和“总需求量”分别加一下。供应量包括风电上限、光伏上限、储能最大放电功率、购电上限需求量包括负荷、储能最小充电如果有。如果某个时段供应量低于需求量那不管约束怎么松都不可能可行。这种情况大概率是原始数据的问题不是模型问题。第二步检查 SOC 终值约束。如果要求 SOC(24) SOC(0)但全天的净充电能力不足这个约束就会导致不可行。最简单的验证方法是先把这个终值约束去掉再跑一遍。如果去掉之后有解说明问题出在储能能量收支上——要么初始 SOC 太低要么充电时间不够。解决办法是把 SOC(0) 调高一点或者放宽终值约束为一个范围而不是固定值。第三步逐条注释约束。从最关键的目标函数开始保留所有变量定义把约束一条条加上去没加一条跑一次。如果某条约束加上之后模型从可行变不可行那问题就锁定在这条约束上。这招听起来土但在模型规模不大的时候非常有效。6.2 求解结果不符合预期怀疑“储能没起作用”很常见的一种情况是模型跑通了成本也降了但画 SOC 曲线一看储能基本没怎么动几乎是一条直线。这时候先别怀疑模型先检查电价的时段差异。如果分时电价在低谷和高峰之间的差距很小比如只差 0.1 元/kWh而储能又有充放电效率损耗5%5%那么套利空间可能根本覆盖不了损耗优化器选择“不让储能工作”其实是最优的。还有一种可能是储能退化成本设置太高。我在初版模型里设了一个相对激进的退化成本系数结果优化器把储能的充放电次数压到了最低SOC 曲线几乎不动。把系数调小一半后储能才开始正常参与调度。所以如果你希望储能承担削峰填谷的角色退化成本系数要谨慎设定最好先跑一组退化成本为零的基准场景看看储能“本来”应该怎么运行再决定系数怎么加。6.3 求解时间过长怎么加速24 时段的模型正常应该在 10 秒以内求解完毕。如果你发现求解时间超过几分钟或者一直跑不到最优解可以从几个方向入手。第一个方向是检查整数变量数量。每个时段的充放电互斥、购售电互斥用掉了 4 个整数变量24 个时段就是 96 个这对求解器来说是很小的规模。但如果你把需求响应也做成了 0-1 决策比如每个时段是否启动响应整数变量数量就会翻倍。如果再加上可平移负荷的整数分配一下子就会增加到上千个。建议优先用连续变量建模需求响应不必要的情况下不要引入额外整数变量。第二个方向是设置求解器参数。我最常用的一招是设置时间上限或 MIP gap 容忍度m.max_mip_gap 0.01 # 允许1%的差距快速得到近似最优解 m.optimize(max_seconds30)对于日前调度这种场景1% 的差距在实际运行中几乎无法感知但求解速度可能是几倍甚至十几倍的差距。工程上完全可以在“最优解的 1% 误差以内”接受结果。第三个方向是精简约束表达。有些约束可以用更紧凑的矩阵形式表达减少求解器的解析负担。Python-MIP 的xsum本质上还是在 Python 层做符号展开变量数量大的时候这部分开销不可忽略。如果模型规模进一步扩大建议转向 Gurobi 等底层优化库直接用 Python 的列表推导式构造约束矩阵在性能上会有明显提升。6.4 数据上下界设置的坑最后说一个非常隐蔽的坑风光预测数据千万不要出现 0 和负数的混用。有些数据集夜间风速很小给出的风电预测值可能是 0.0001有些数据源处理时把这个值直接标成了负数或者 NaN。如果不在数据预处理阶段统一清洗给lb0, ub负数的变量会让求解器直接报错。我的习惯是数据读进来之后立即把所有小于 0 的预测值统一置 0对所有缺失值用前后时段均值填充。这个过程写成一个clean_forecast_data()函数所有数据源进来都先过这一关后面模型跑起来会省心很多。写在最后的小经验做完这个项目我最大的感受是微电网日前经济调度的问题核心不在算法而在建模的取舍。风光曲线该当上限还是固定值、储能要不要加退化成本、需求响应用连续变量还是整数变量——这些决策每一个都会显著影响结果但没有任何教科书能替你决定只能靠对物理系统的理解和对数据的敏感性来拍板。如果你打算在自己的项目里复现这套模型我建议不要直接跑完整代码。先把数据备齐画好曲线手动思考一遍“如果我是调度员我会怎么安排明天的运行”再回头对照模型的输出。当你发现模型给出的方案和人工判断一致的时候说明你的模型逻辑是对的当两者出现冲突的时候恭喜你那正是深入理解问题的好机会。这比调任何参数都有价值。
阅读完成 · 觉得有帮助?
咨询建站