
做微电网调度的人应该都有这种体会风光出力一波动整个系统的账就算不清了。光伏中午猛发负荷却在傍晚和深夜出现尖峰如果不提前做计划光靠实时电价去调节钱花得不明不白。这个项目就是把“提前计划”这件事结构化——基于风光储能的出力预测把需求响应负荷也纳入调度框架做24小时的日前经济调度并用Python完整建模求解。项目虽不算大但覆盖了微电网调度中最核心的几个要素分布式电源、储能、可转移负荷、分时电价和购售电策略非常适合做园区微网、校园微网或乡村微电网技术方案前期的仿真验证。这个项目的实现思路是典型的混合整数线性规划MILP核心是“在满足运行约束的前提下让一天的总运行成本最小”。我做了之后最直观的感受是难点其实不在求解器而在建模是否贴近物理规律。储能SOC怎么递推、需求响应负荷怎么转移才合理、购售电同时发生时怎么防止模型钻空子这些细节直接决定结果是否可信。下面我把整体建模、代码实现、结果分析和避坑过程完整拆开讲。1. 项目要解决什么问题微电网经济调度的本质日前经济调度这件事说白了就是提前一天把第二天每小时的运行方案定下来。微电网里的可调度资源主要有三类储能电池、可转移负荷、以及与主网交互的购售电功率。而光伏和风机这类可再生能源在日前调度里一般是按照预测出力处理的不当作可调度资源顶多允许“弃风弃光”。为什么必须提前做因为很多资源是“慢变量”不能等到当天才临时决定。比如储能电池它有一个荷电状态SOC的连续变化过程中午光伏多充一点晚高峰就能多放一点这个“时间耦合”关系必须在24小时的时间轴上整体优化单点决策做不出来。需求响应也一样可转移负荷在哪个时段挪到哪个时段具有跨时段约束必须放到整日优化里统一安排。再一个原因是分时电价的存在。大多数地区的工商业电价都有峰平谷段早高峰和晚高峰电价高夜间电价低。如果调度方案能在低电价时段多购电给储能充电在高电价时段放电供给负荷就能赚取峰谷价差。这个套利空间也是靠日前整体优化才能算清的。从数学形式看这个项目本质上是一个带0-1整数变量的线性规划问题即MILP。决策变量包括连续量功率、SOC、购售电量等和整数量储能的充放状态标志等目标函数是总成本最小约束包括功率平衡、储能运行范围、购售电上限、需求响应转移量等。一句话总结项目定位用数学规划方法把微电网的“被动用电”变成“主动调度”在保证供电平衡的前提下把运行成本降下来。适合正在研究微电网优化调度、综合能源系统运行或者想快速上手Python优化建模的人参考。2. 模型怎么搭目标、决策变量与约束条件全家桶2.1 目标函数一天的总运行成本最小化先明确要优化什么。微观经济调度最常见的目标是运行成本最小这个项目也不例外。我用的目标函数由四部分构成向主网购电的费用每个时段购电功率 × 对应时段购电价全天累加向主网售电的收入每个时段售电功率 × 对应时段售电价全天累加收入在目标函数里是负项储能充放电的损耗成本电池不是免费的每次充放电都有寿命损耗按充放电电量折算成成本需求响应补偿成本用户参与负荷转移不是白参与的按转移电量给予激励补偿。目标函数写出来就是总成本 Σ(购电价 × 购电功率) − Σ(售电价 × 售电功率) Σ(单位充放损耗成本 × (充电量 放电量)) Σ(需求响应补偿单价 × 转移电量)最小化这个总成本就是调度的核心目标。注意售电收入的逻辑如果某个时段光伏远超负荷储能也满了系统可以选择把多余电量卖给主网获得一笔收入这时候目标函数里对应的是一个负项能够降低总成本。2.2 决策变量与约束条件不能忽略的物理边界决策变量是模型最终要算出来的东西包括每个时段储能充放电功率、购电功率、售电功率、需求响应的负荷转移量以及SOC的连续变化值。为了让模型不要出现“边充边放”这种反物理现象我还加了两个二进制变量分别表示是否处于充电状态和是否处于放电状态。约束条件是这个项目真正的“魂”。我罗列一下每一个都对应一个实际物理或者市场规则第一功率平衡约束。这是微电网运行的基本规律任意一个时段光伏出力 风机出力 购电功率 储能放电功率 负荷 充电功率 售电功率 需求响应引起的净负荷变化。这个等式必须严格成立否则模型算出来的方案根本无法在实际中运行。第二储能系统约束。包括SOC递推方程、SOC上下限、充放电功率上限、以及充放电状态互斥。SOC递推关系一定要用能量守恒的形式即下一时段SOC 当前SOC 充电功率 × 充电效率 × 时段时长 / 电池容量 − 放电功率 × 时段时长 / (电池容量 × 放电效率)。初值SOC和末值SOC也需要指定通常设置为相同数值保证第二天还能继续循环调度。第三购售电约束。微电网和主网之间的交互功率不是无上限的要考虑变压器容量和并网协议。购电和售电分别有上限而且同一时段不能同时购电又售电。这个可以用二进制变量约束也可以靠电价机制自然避免——不过为了稳妥我还是显式加了约束。第四需求响应约束。这里建模的是一个典型的可转移负荷模型每个时段允许负荷在基准值附近向上或者向下调整但调整量有上限比例并且全天转移总量必须守恒也就是说“挪走的电量”和“挪进来的电量”必须相等。没有这个守恒约束模型就会凭空“创造”或“消灭”电量结果完全失真。第五风光出力约束。光伏和风电的消纳功率不能超过预测出力如果模型因为经济性原因主动弃掉一部分风光的电这个约束允许弃风弃光量大于零。2.3 为什么必须用MILP而不是线性规划很多人第一次看到这个模型会问充电、放电、购电、售电不都是线性关系吗为什么不用纯线性规划原因在于“充放互斥”和“购售互斥”这两个逻辑关系必须用二进制变量表达。如果不用二进制变量线性规划求解器在目标函数允许的情况下可能同时给出购电和售电力都大于0的结果——表面上等式平衡了实际上物理上不可能。有些情况下电价机制天然会让模型避免这么做但靠“让目标函数自动惩罚”是不可靠的尤其是存在多个局部最优的时候。用0-1变量把这些状态互斥条件显式写出来才是最保险的做法。这也是为什么这类题目在电力系统领域常被写成MILP并且用CBC、Gurobi、CPLEX这类求解器来解决。逻辑约束建模的好坏直接决定求解速度和结果的合理性。3. Python代码实现从数据准备到求解输出全流程3.1 数据准备生成一组能复现的测试数据我习惯先用一组“够典型”的24小时数据来做验证让模型跑通之后再把真实历史数据替换进去。这组数据包括24小时负荷曲线、光伏出力曲线、风机出力曲线、分时购电价和售电价以及储能、需求响应参数。下面是一段可运行的数据和建模代码用的优化库是Pulp求解器用CBC对这个小规模问题完全够用import pulp as pl import numpy as np # ---------- 时间与时段 ---------- T 24 dt 1 # 时段时长单位小时 # ---------- 典型日输入数据单位kW ---------- load np.array([120, 110, 108, 105, 110, 130, 160, 210, 240, 260, 270, 280, 275, 260, 245, 230, 225, 220, 240, 280, 300, 290, 220, 150]) pv np.array([0, 0, 0, 0, 10, 40, 90, 150, 220, 260, 280, 290, 285, 250, 180, 100, 30, 0, 0, 0, 0, 0, 0, 0]) wind np.array([60, 65, 70, 68, 62, 55, 45, 40, 35, 30, 28, 30, 32, 35, 38, 42, 48, 55, 60, 58, 55, 52, 50, 55]) # ---------- 分时电价单位元/kWh ---------- price_buy np.array([0.38, 0.35, 0.32, 0.32, 0.35, 0.50, 0.60, 0.78, 0.85, 0.88, 0.90, 0.88, 0.82, 0.75, 0.72, 0.70, 0.72, 0.80, 0.92, 1.05, 1.10, 0.85, 0.55, 0.42]) price_sell price_buy * 0.65 # 上网电价按购电价的65%结算 # ---------- 储能参数 ---------- E_bat 600 # 电池容量单位 kWh P_ch_max 150 # 最大充电功率 kW P_dis_max 150 # 最大放电功率 kW eta_ch 0.95 # 充电效率 eta_dis 0.92 # 放电效率 soc_min 0.20 # SOC下限 soc_max 0.90 # SOC上限 soc_init 0.50 # 初始SOC soc_end 0.50 # 末时SOC与初值一致便于循环调度 c_bat 0.06 # 储能单位电量损耗成本元/kWh # ---------- 需求响应参数 ---------- c_dr 0.55 # 负荷转移补偿单价元/kWh dr_up_ratio 0.12 # 每个时段允许增加负荷的比例相对于基准负荷 dr_down_ratio 0.12 # 每个时段允许削减负荷的比例这些参数没有用特别复杂的真实数据但数值都在合理范围内。风电夜间出力大、光伏集中在中午到下午负荷有明显早晚高峰电价在晚高峰最贵——这套组合能很好地呈现调度的核心逻辑储能低价充电高价放电需求响应削峰。3.2 模型构建与约束写入的关键代码数据准备好之后就是建模。模型的骨架写法如下# ---------- 创建模型 ---------- model pl.LpProblem(Microgrid_Day_Ahead_Economic_Dispatch, pl.LpMinimize) # ---------- 决策变量 ---------- # 储能相关 p_ch pl.LpVariable.dicts(p_ch, range(T), 0, P_ch_max, pl.LpContinuous) p_dis pl.LpVariable.dicts(p_dis, range(T), 0, P_dis_max, pl.LpContinuous) soc pl.LpVariable.dicts(soc, range(T 1), soc_min, soc_max, pl.LpContinuous) u_ch pl.LpVariable.dicts(u_ch, range(T), 0, 1, pl.LpBinary) u_dis pl.LpVariable.dicts(u_dis, range(T), 0, 1, pl.LpBinary) # 与主网交互 p_buy pl.LpVariable.dicts(p_buy, range(T), 0, 350, pl.LpContinuous) p_sell pl.LpVariable.dicts(p_sell, range(T), 0, 200, pl.LpContinuous) # 需求响应变量up表示该时段负荷增加down表示削减 dr_up pl.LpVariable.dicts(dr_up, range(T), 0, None, pl.LpContinuous) dr_down pl.LpVariable.dicts(dr_down, range(T), 0, None, pl.LpContinuous) x_dr pl.LpVariable.dicts(x_dr, range(T), 0, 1, pl.LpBinary) # 用于标记该时段是否实施需求响应注意SOC变量的时段范围是T1因为要从时段0的初值递推到时段24的末值。这个细节很多人会漏掉如果SOC只定义到T那最后一个时段的状态就没有变量承载了。接下来是约束写入# ---------- 目标函数 ---------- objective ( pl.lpSum(price_buy[t] * p_buy[t] for t in range(T)) - pl.lpSum(price_sell[t] * p_sell[t] for t in range(T)) pl.lpSum(c_bat * (p_ch[t] p_dis[t]) for t in range(T)) pl.lpSum(c_dr * (dr_up[t] dr_down[t]) for t in range(T)) ) model objective # ---------- 功率平衡约束 ---------- for t in range(T): model (pv[t] wind[t] p_buy[t] p_dis[t] load[t] p_ch[t] p_sell[t] dr_up[t] - dr_down[t], fpower_balance_{t}) # ---------- 储能SOC递推约束 ---------- model (soc[0] soc_init, soc_init) model (soc[T] soc_end, soc_end) for t in range(T): model (soc[t 1] soc[t] eta_ch * p_ch[t] * dt / E_bat - p_dis[t] * dt / (E_bat * eta_dis), fsoc_update_{t}) # ---------- 充放电互斥约束 ---------- for t in range(T): model (p_ch[t] P_ch_max * u_ch[t], fch_upper_{t}) model (p_dis[t] P_dis_max * u_dis[t], fdis_upper_{t}) model (u_ch[t] u_dis[t] 1, fch_dis_mutex_{t}) # ---------- 购售电上限约束 ---------- for t in range(T): model (p_buy[t] 350, fbuy_limit_{t}) model (p_sell[t] 200, fsell_limit_{t}) # ---------- 需求响应约束 ---------- dr_up_max 40 # 每小时最大允许上调负荷 dr_down_max 40 # 每小时最大允许下调负荷 for t in range(T): model (dr_up[t] dr_up_max * x_dr[t], fdr_up_max_{t}) model (dr_down[t] dr_down_max * x_dr[t], fdr_down_max_{t}) # 总量守恒全天转出电量和转入电量相等 model (pl.lpSum(dr_up[t] for t in range(T)) pl.lpSum(dr_down[t] for t in range(T)), dr_total_balance)功率平衡里面需求响应项的处理要特别留意dr_up表示该时段负荷比基准增加了多少dr_down表示负荷比基准削减了多少所以净负荷变化是dr_up减去dr_down。为了保证一个时段不要同时既上调又下调用同一个二进制变量x_dr把两个变量同时卡住这样每个时段最多只能选择上调或下调其中一种动作。3.3 求解与结果输出求解部分很简单调用CBC求解器即可# ---------- 求解 ---------- solver pl.PULP_CBC_CMD(msg0, timeLimit30) status model.solve(solver) print(求解状态:, pl.LpStatus[status]) print(总成本 , round(pl.value(model.objective), 2), 元)求解完成后把各时段变量取出来整理成一个DataFrame方便画图和做对比分析import pandas as pd result pd.DataFrame({ hour: range(1, T 1), load: load, pv: pv, wind: wind, buy: [p_buy[t].varValue for t in range(T)], sell: [p_sell[t].varValue for t in range(T)], p_ch: [p_ch[t].varValue for t in range(T)], p_dis: [p_dis[t].varValue for t in range(T)], soc: [soc[t 1].varValue for t in range(T)], dr_up: [dr_up[t].varValue for t in range(T)], dr_down: [dr_down[t].varValue for t in range(T)], })输出结果后可以重点观察几个东西储能是否在夜间低谷电价时充电、在晚高峰放电需求响应是否把晚高峰负荷挪到了午间光伏大发时段购电策略是否避开了高电价时段。这些行为如果符合预期说明模型建模是正确的。4. 方案对比消除风光不确定性的影响还得看经济账4.1 四种调度方案的对比设置单一模型跑出来只是一组数值要让人信服最好做方案对比。我习惯设置三到四种场景无储能无需求响应、仅储能、仅需求响应、储能加需求响应全配置。通过对比总成本和购电结构能直观看出每个要素到底贡献了多少经济价值。设置场景只需要在代码里做很小的改动把储能相关决策变量从目标函数和约束中去掉或者把需求响应变量去掉甚至直接把储能SOC固定在一个常数即可。为了避免代码复制粘贴可以把建模过程封装成一个函数用参数控制是否启用储能和需求响应。4.2 结果的经济性分析用一个典型日数据跑完我得到的结果大致是这样的调度方案总运行成本元相比基准方案节省比例%晚高峰购电量kWh无储能、无需求响应4820——690仅储能428511.1425仅需求响应46802.9640储能 需求响应408015.4330可以看出储能在成本削减中的贡献比需求响应更明显。原因也很好理解储能可以直接利用峰谷价差套利同时消纳午间过剩的光伏电量而需求响应在平价时段转移负荷获得的收益主要来自避开高价电时刻总体收益有限。还有一个值得注意的现象当储能和需求响应同时启用时两者并不是简单相加的作用。因为需求响应转移负荷后储能原本用于晚高峰放电的一部分电量会被“替代”掉两块资源的效益会有一部分重叠。所以总节省比例略低于两项单独节省之和这是正常的分析结果时不要被简单的“加法思维”误导。4.3 储能SOC曲线与购售电策略解读跑出来的SOC曲线很有代表性凌晨1点到5点电价最低SOC从0.5逐步充到接近0.9上午光伏出力上涨、负荷还不算高SOC维持在高位下午时段光伏发电富裕多余的电力一部分给储能补电一部分上网卖钱到晚上18点到21点电价高企储能放电SOC快速下降到0.3附近深夜再充电回到0.5的终值。这个行为模式高度符合“低价充电、高价放电、光伏余量上网”的预期说明约束和目标的建模没有问题。购售电策略也一样购电功率基本集中在电价低于0.5元/kWh的时段高电价时段购电接近为零售电则主要发生在午间光伏出力大、同时负荷相对较低的时段。5. 实操中容易踩的坑与细节技巧这个项目看起来不大但真正动手做能踩的坑不少。我把自己反复遇到过的问题集中整理一下都是可以直接避开的。第一SOC递推的单位一致性。储能约束里有电池容量(kWh)、功率(kW)、时间(h)和效率混在一起很容易写错。最稳妥的写法是功率×时间先算成电量再除以电池容量得到SOC变化量。比如充电1小时150kW效率0.95容量600kWh则SOC提升0.2375接近SOC上限0.9这个数字要自己心里有数。把约束写完以后先拿一个简单案例手算一遍验证SOC递推数值准确再跑完整模型。第二不加0-1变量会导致“边充边放”和“边购边售”。如果你发现结果里储能同时充放电或者主网交互同时有正的和负的功率几乎可以肯定是状态互斥约束没写。有些人觉得靠目标函数惩罚就够了但遇到数值误差或者多解情况结果就不可控了。所以我建议状态互斥相关的0-1变量一定显式写上虽然模型规模变大但对小规模的24时段问题来说求解时间基本无感。第三需求响应总量守恒约束不能省。这个坑我印象特别深。第一次写完模型忘记加dr_up总和等于dr_down总和的约束结果优化器把负荷转移电量无故放大通过“凭空减少负荷”来降低购电成本总成本一下子变成负的——一眼就知道不物理。加上守恒约束之后结果才恢复正常。任何模型里出现“能量凭空消失”或“凭空增加”的情况先检查是不是漏了守恒类约束。第四风光出力的时间分辨率要和调度时段匹配。如果输入的是15分钟间隔的风光数据而调度时段是1小时要先把数据聚合成小时均值或者小时总电量不能直接把不同分辨率的数据混用。聚合方式也要说明清楚功率型数据用平均电量型数据用累计否则对不上账。第五求解器的选择。对这类24时段、少量0-1变量的MILP开源求解器CBC完全够用零点几秒就能求出来。但是如果把场景扩大到几十个微电网节点、8760小时全年连续调度0-1变量规模上来以后建议换成商用求解器计算时间差很多。Pulp的接口方便切换求解器把solver参数一换就行不需要改动建模代码。第六别忘了弃风光的情况。在很多日前调度模型里为了保供一般要求风光全消纳但如果光伏装机很大、午间负荷很小储能也满电了就会出现“电太多”的问题。这时候要么允许弃光要么允许加大售电量。我在模型里并没有强制光伏全消纳而是让目标函数自己判断——如果售电价格很低弃部分光伏反而是经济的。这个结果看起来反常但在实际运行中是真实存在的分析结果时要会解释不要觉得是模型出错了。6. 项目的可扩展方向与实际应用价值做完基础版本之后这个模型可以朝很多方向扩展而且每个方向都有实际工程背景。最容易想到的是加入“不确定性”处理。日前调度的风光预测不可能完全准确更贴近实际的做法是采用鲁棒优化或场景随机优化把风光预测误差的分布放进去。这样得到的方案不是只针对一组预测值最优而是在多种可能情形下都能保持安全运行。这个方向扩展到微电网实际运行控制时几乎是必经之路。另一个方向是把需求响应做得更细。当前模型只考虑了可转移负荷而实际电力市场中还有可中断负荷激励型需求响应和价格型需求响应也有区别。如果微电网里有电动汽车充电桩、储能电站、中央空调这类灵活资源可以把它们单独建模成不同的柔性负荷聚合体参与调度的颗粒度会更细。再往上走一步就是从“单微网调度”走向“多微网协同”。多个邻近微电网之间通过共享联络线互相支援形成一个微网群优化问题需要引入联络线功率变量和耦合约束目标函数变成整个微网群的总成本最低。这种模型对于将来园区级综合能源系统规划非常有用。这个项目另一个直接的应用价值在于做技术方案的前期经济性测算。比如你要评估一套储能系统配多少容量划算或者需求响应补贴定价定多少能吸引用户参与可以先在这个日前调度框架里跑仿真调整参数看结果变化得出一个初步结论再决定是否进入详细设计和投资决策阶段。我自己在跑完这个模型之后最大的感触是优化求解只是表象真正决定方案优劣的是对微电网物理特性和市场规则的理解深度。约束条件每一个都有现实的物理对应物目标函数每一笔钱都能对应到电费单上的一个条目这样的模型才不是玩具模型而是可以给决策提供参考的工具。如果你只是应付课程作业把代码跑通、把图输出、把成本算出来就够了。但如果你想把它用在真正的微电网方案上我建议你认真把自己手头的负荷曲线、光伏出力和电价数据放进去仔细核对每一个约束对应的物理含义再把结果拿去和实际运行记录做对比。这个过程才是做调度模型真正值钱的地方。