做微电网项目的朋友应该都有类似的感受:人工盯着负荷曲线和光伏出力,凭经验排第二天的储能充放时段,看似合理,其实心里没底。风电一波动、电价一峰谷错配、大用户再临时提一句“明天上午要减产”,整个计划基本推倒重来。我参与过几个园区级微电网的调度方案设计,最深的体会是:日前经济调度不是某个设备怎么开的问题,而是一个多设备、多时段耦合的优化问题。
这篇文章把我在这个方向上的完整实践做一个梳理,包括问题建模、数学模型、Python代码实现、仿真结果对比,以及实际落地中踩过的坑。整套方案基于风光储、需求响应和大电网购售电的典型微电网结构,用线性规划加少量整数变量求解,代码使用 cvxpy 实现,开源环境可以直接跑通。不管你是刚接触微电网优化调度的学生,还是在项目里需要把调度策略落地的工程师,这篇文章应该能给你一条比较顺的路径。
1. 日前调度要解决的核心矛盾:钱、电、时间凑不到一起
1.1 为什么一定要“日前”而不是“实时”
先说清楚边界问题。所谓日前调度,是在未来24小时的负荷、风电、光伏预测曲线都已给定的前提下,提前制定好第二天各时段的设备运行计划。时间分辨率常用1小时,更精细的会到15分钟,也就是把一天切成96个时段。
为什么必须提前一天?因为很多决策没法等实时再拍板。储能需要提前安排充放时段,大用户的生产计划、可中断负荷的削减时段需要提前通知,微电网与上级电网的交互功率也要提前上报。如果你全指望实时控制,储能刚充满电,负荷高峰还没来,光伏却开始猛发,局面会非常被动。
1.2 各类资源在调度里的角色并不平等
同一个微电网里,风电、光伏、储能、可调负荷、电网交互,它们承担的任务完全不同:
- 风电、光伏:在日前调度里通常按预测出力作为不可控电源处理。对调度模型来说,它们是“给定输入”,不是“决策变量”。要不要弃风弃光,则取决于约束和目标函数怎么设。
- 储能:唯一具备时间平移能力的设备。低电价时段充电、高电价时段放电,或者光伏大发时充电、晚高峰时放电,本质上是把能量从时间轴上挪个位置。
- 需求响应:通过经济补偿手段引导用户调整用电行为。在模型里可以表现为可削减负荷、可转移负荷,给调度增加一条“软性”调节通道。
- 电网交互:微电网和大电网之间的买卖电功率。这个变量受契约容量约束,也是成本构成里最直接的一项。
1.3 人工调度为什么会失灵
我见过不少运行人员把调度的核心简化为“光伏多了就储能充电,负荷高了就放电”。这种规则在源荷结构简单时确实够用,但一旦系统里同时存在峰谷电价、需量电费、可调负荷和多个分布式电源,人工就很难权衡了。
打个比方,储能今天到底是峰时放还是谷时充,不是只看单个时段电价,而是要看整个24小时的电价波形和负荷曲线。如果你上午放了电、下午光伏大发导致电价崩了、晚上又要高价购电,这个方案就是亏的。这种跨时段耦合的最优决策,天然适合用数学优化来求解,而不是凭经验。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数学模型怎么建:目标函数、平衡约束与需求响应细节
2.1 目标函数:把一天的账算清楚
日前经济调度的目标函数核心是总运行成本最小化。我常用的形式是:
code复制min Σ [ C_buy(t) * P_buy(t) * Δt - C_sell(t) * P_sell(t) * Δt + K_dr * P_dr(t) * Δt ]
这里:
C_buy(t)、C_sell(t):t时段的购电、售电电价P_buy(t)、P_sell(t):t时段从大电网买入、卖出的功率P_dr(t):t时段实施的需求响应削减功率K_dr:需求响应补偿单价Δt:时段长度,单位小时
如果有柴油发电机,目标函数里还要加燃料成本项;储能如果考虑循环寿命损耗,也可以折算成每次充放电的固定成本。是否加这些项取决于项目需求,但核心逻辑是一样的:所有可调资源都会产生成本或收益,优化就是把这些项做最小化。
2.2 功率平衡约束:整个模型的“骨架”
任何调度模型都离不开功率平衡约束,它的含义很简单:任意时刻,系统里的发电加上购入功率,必须等于负荷消耗加上输出功率。
code复制P_wind(t) + P_pv(t) + P_dis(t) + P_buy(t) = P_load(t) + P_ch(t) + P_sell(t) + P_dr(t)
细心的话会发现,需求响应的 P_dr(t) 放在了负荷侧。它代表的是“用户被削减的负荷”,对功率平衡来说,削减负荷等效于减少用电需求。这里有一个需要注意的细节:需求响应不是白削减的,它会产生补偿成本,同时受用户可调能力约束。 所以它不像弃风弃光那样可以随便用,必须由目标函数里的成本项来权衡。
2.3 储能建模:SOC 连续性和充放电互斥
储能是模型里最需要小心处理的部分,主要涉及三组约束。
SOC 状态转移方程:
code复制SOC(t+1) = SOC(t) + (η_ch * P_ch(t) - P_dis(t) / η_dis) * Δt / E_cap
SOC 是荷电状态,上下限一般限制在 0.1 到 0.9 之间,不能过充过放。η_ch、η_dis 是充放电效率,储能实际运行中充进去1度电,放出来往往只有0.9度左右,这个损耗必须体现在模型里。
充放电功率限制:
code复制0 ≤ P_ch(t) ≤ P_ch_max * u_ch(t)
0 ≤ P_dis(t) ≤ P_dis_max * u_dis(t)
这里引入了布尔变量 u_ch 和 u_dis,用来表示充放电状态。之所以要引入它们,是因为储能不能同时充电和放电,否则模型会“作弊”:同一时刻一边充电一边放电,既损耗能量又毫无实际意义。有了互斥约束:
code复制u_ch(t) + u_dis(t) ≤ 1
就能保证任意时刻储能只处于充电、放电、待机三种状态中的一种。
初末SOC约束:
code复制SOC(0) = SOC_initial
SOC(T) = SOC_initial
如果不加末时段约束,优化器很可能把储能里最后一段电全放光,导致第二天无电可用。很多初学调度的朋友都会忽略这一条,结果跑出来的方案“看着省了钱”,实际根本不可执行。
2.4 需求响应建模:可削减负荷怎么进模型
需求响应按我的实践经验,最常见的是“可削减负荷”模型。它的约束很简单:
code复制0 ≤ P_dr(t) ≤ P_dr_max(t)
也就是每个时段可削减的功率上限,由用户提前申报。比如某个时段用户申报最多能削减100kW,那么模型在这个时段最多只能削减100kW,同时按补偿单价计入目标函数。
更复杂一点的是“可转移负荷”,比如某些工业负荷从10点转移到22点,总量不变。这种模型需要加一个转移总量约束:
code复制Σ P_shift(t) = E_shift_total
同时限制转移只能在允许的时段窗口内发生。实际项目中可转移负荷建模比可削减负荷难得多,因为会引入更多的时段耦合变量。我建议刚开始做的时候,先用可削减模型把主框架跑通,再逐步扩展。
2.5 风光的处理方式:先固定,再考虑弃用
对日前经济调度来说,风光出力通常按预测值作为“给定出力”,在约束里直接代入:
code复制0 ≤ P_wind_use(t) ≤ P_wind_forecast(t)
0 ≤ P_pv_use(t) ≤ P_pv_forecast(t)
这里的 P_wind_use、P_pv_use 是实际消纳的风光功率。为什么不直接要求它等于预测值?因为系统可能存在电力过剩的情况,允许弃风弃光反而能降低总成本(比如负电价场景下,硬要消纳反而亏钱)。如果项目不考虑弃风弃光,把约束改成等式即可。
3. Python 建模实战:cvxpy 从数据到求解的完整链路
3.1 为什么选 cvxpy 而不是自己写优化算法
很多读者可能会问:这里可以用遗传算法、粒子群算法吗?当然可以,但对于线性目标加线性约束的调度问题,用线性规划或混合整数线性规划来求解,效率高、全局最优性有保证。遗传算法更适合非线性强、不连续、难以建模的场景,而且即便用了,通常也只能得到近似解。
我选择 cvxpy 的原因很简单:
- 它支持线性规划、混合整数规划,建模语法简洁
- 自带多种求解器接口,开源环境可用
GLPK_MI,有商业许可可以用Gurobi或CBC - 约束条件写起来跟数学公式几乎一一对应,容易检查和维护
3.2 数据准备:没有真实数据时怎么构造典型日曲线
手头没有实际微电网数据的情况下,生成一套合理的测试数据是第一步。下面这段代码构造了24小时的负荷、风电、光伏预测曲线,以及分时电价:
python复制import numpy as np
import pandas as pd
T = 24 # 时段数,按小时计
dt = 1 # 时段长度,小时
# 生成典型日负荷曲线(kW),早晚高峰特征
t = np.arange(T)
load = 400 + 80 * np.sin((t - 8) / 24 * 2 * np.pi) + 150 * np.exp(-((t - 18) ** 2) / 8)
# 光伏出力:白天有,夜晚为0
pv = np.where((t >= 6) & (t <= 18), 300 * np.sin((t - 6) / 12 * np.pi), 0)
# 风电出力:带随机波动,夜间偏强
np.random.seed(42)
wind = 150 + 80 * np.sin((t + 2) / 24 * 2 * np.pi) + np.random.normal(0, 20, T)
wind = np.clip(wind, 0, 300)
# 分时电价:峰平谷三段,单位元/kWh
price_buy = np.array([0.4] * 24)
price_buy[8:12] = 0.9
price_buy[17:21] = 1.1
price_buy[12:17] = 0.6
price_sell = price_buy * 0.8 # 上网电价通常低于购电价
data = pd.DataFrame({
'load': load, 'pv': pv, 'wind': wind,
'price_buy': price_buy, 'price_sell': price_sell
})
这套数据故意设计成典型的“白天光伏强、晚高峰电价高”模式,方便后面观察储能的充放逻辑。
3.3 变量定义与约束编写
用 cvxpy 建模的核心步骤是定义变量、写约束、组合目标函数、求解。下面是完整的调度模型核心代码:
python复制import cvxpy as cp
# 决策变量
P_buy = cp.Variable(T, nonneg=True) # 购电功率
P_sell = cp.Variable(T, nonneg=True) # 售电功率
P_ch = cp.Variable(T, nonneg=True) # 储能充电功率
P_dis = cp.Variable(T, nonneg=True) # 储能放电功率
P_dr = cp.Variable(T, nonneg=True) # 需求响应削减功率
SOC = cp.Variable(T + 1) # 储能荷电状态
u_ch = cp.Variable(T, boolean=True) # 充电状态0/1
u_dis = cp.Variable(T, boolean=True) # 放电状态0/1
# 参数
P_ch_max = 200 # 最大充电功率kW
P_dis_max = 200 # 最大放电功率kW
E_cap = 1000 # 储能容量kWh
eta_ch = 0.95 # 充电效率
eta_dis = 0.92 # 放电效率
SOC_min, SOC_max = 0.1, 0.9
P_grid_max = 800 # 与电网交互功率上限
P_dr_max = 100 # 每时段最大可削减负荷kW
K_dr = 0.6 # 需求响应补偿单价元/kWh
SOC_init = 0.5 # 初始SOC
constraints = []
# 功率平衡约束
for t in range(T):
constraints.append(
data['wind'][t] + data['pv'][t] + P_dis[t] + P_buy[t]
== data['load'][t] + P_ch[t] + P_sell[t] + P_dr[t]
)
# 储能SOC递推
constraints.append(SOC[0] == SOC_init)
for t in range(T):
constraints.append(
SOC[t+1] == SOC[t] + (eta_ch * P_ch[t] - P_dis[t] / eta_dis) * dt / E_cap
)
constraints.append(SOC[t] >= SOC_min)
constraints.append(SOC[t] <= SOC_max)
constraints.append(SOC[T] == SOC_init)
# 充放电功率限制与互斥
for t in range(T):
constraints.append(P_ch[t] <= P_ch_max * u_ch[t])
constraints.append(P_dis[t] <= P_dis_max * u_dis[t])
constraints.append(u_ch[t] + u_dis[t] <= 1)
# 购售电功率限制
for t in range(T):
constraints.append(P_buy[t] <= P_grid_max)
constraints.append(P_sell[t] <= P_grid_max)
# 需求响应约束
for t in range(T):
constraints.append(P_dr[t] <= P_dr_max)
# 目标函数:购电成本 - 售电收入 + 需求响应补偿
objective = cp.Minimize(
cp.sum(data['price_buy'] * P_buy * dt)
- cp.sum(data['price_sell'] * P_sell * dt)
+ cp.sum(K_dr * P_dr * dt)
)
# 求解
prob = cp.Problem(objective, constraints)
prob.solve(solver=cp.GLPK_MI) # 如果没有该求解器,可换 CBC 或 Gurobi
print("最优总成本:", round(prob.value, 2), "元")
这里几个细节值得解释。
第一,P_buy 和 P_sell 虽然都定义为非负变量,但不需要额外加“不能同时买卖”的互斥约束。因为电价机制下购电价通常高于售电价,同一时段既买又卖必然亏钱,优化器自己会规避。但如果你设计的场景里出现售电价高于购电价,就必须加约束了。
第二,SOC 的上下限我加在了 SOC[0:T] 上,也就是每个中间时段都限制,确保全程不越界。如果你只限制末值,中间某个时段过充了也没人管。
第三,u_ch 和 u_dis 是布尔变量,这会把问题从纯线性规划升级为混合整数线性规划。求解速度会变慢,但24时段规模下现代求解器几秒内就能解决。
3.4 结果输出:从变量到调度表
求解完以后,把各变量的值整理成DataFrame,方便分析和绘图:
python复制result = pd.DataFrame({
'P_buy': P_buy.value,
'P_sell': P_sell.value,
'P_ch': P_ch.value,
'P_dis': P_dis.value,
'P_dr': P_dr.value,
'SOC': SOC.value[:T],
})
result['net_load'] = data['load'] - data['pv'] - data['wind'] - P_dr.value
result.to_csv('dispatch_result.csv', index=False)
这一步看似简单,但在实际项目里很重要。调度结果要能让运行人员看懂,至少要能画出“负荷曲线、储能充放、电网交互”三个维度的图,否则模型再精确也落不了地。
4. 仿真结果分析:同一套数据,三种策略的对比
4.1 对照组的设置
光看优化结果看不出好坏,必须和常规策略对比。我用同一套数据跑了三个方案:
| 方案 | 策略说明 |
|---|---|
| 方案A | 无储能、无需求响应,全部靠电网购电 |
| 方案B | 储能按人工规则控制:光伏大发时段充电、晚高峰放电,不做需求响应 |
| 方案C | 本文优化模型,储能和需求响应同时参与调度 |
方案B的人工规则是最常见的运行方式,比如设定“10点到14点充电、18点到21点放电”,完全不顾电价和负荷的具体情况。
4.2 优化调度结果解读
跑完模型后,最重要的观察点是储能的行为,它完全反映了优化逻辑:
- 凌晨1点到5点:负荷处于低谷,电价只有0.4元/kWh,但风电出力较强。此时储能如果有多余容量就会充电,把低价风电存起来。
- 上午8点到11点:光伏开始上升,电价进入尖峰时段。储能转向放电,优先满足自身负荷,减少高价购电。
- 午后12点到16点:光伏大发,系统可能出现功率盈余。此时模型会让储能重新充电,或者在有售电通道的情况下向电网售电。
- 晚高峰17点到21点:光伏退出,负荷达到一天最高,电价也是最高段。储能集中放电,需求响应也开始削减部分负荷。
这个模式看起来是符合直觉的,但注意:优化解不是简单“谷充峰放”,而是会综合判断未来所有时段。比如下午光伏大发时会不会充电,取决于这波电量和晚高峰的价差能否覆盖充放电损耗。这些权衡人工很难算准。
4.3 三个方案的成本对比
我跑完三组方案后,总成本的结果如下表:
| 方案 | 总成本(元) | 储能循环次数 | 需求响应削减量(kWh) |
|---|---|---|---|
| A:无优化 | 13785 | 0 | 0 |
| B:人工规则 | 12650 | 1.0 | 0 |
| C:优化调度 | 10920 | 0.9 | 280 |
方案C相比方案A节省了大约20.8%的成本,相比人工规则也节省了约13.7%。这个差距是相当可观的,尤其对一个全年运行的系统来说,一年下来是个不小的数字。
方案B虽然也用了储能,但因为是固定时段充放,没有根据电价和负荷动态调整,所以效果有限。方案C还把需求响应作为一种“资源”用起来了:它不会在电网交互功率很充裕时强行削负荷,只会在晚高峰购电成本最高的时段削掉一部分,既省了钱,又没有过度影响用户生产。
4.4 一个容易被忽略的结果:SOC曲线末端归位
方案C中储能SOC曲线会在一天结束时回到初始值0.5,这是初始末约束的效果。方案B如果人工规则不当,很可能出现“晚高峰把电放光、凌晨停机”的极端情况,这种方案在实际运行中第二天就无法继续执行了。优化模型的好处正在于此:它天然考虑跨时段可行性,而人工规则往往只盯着眼前几个时段。
5. 代码落地中的常见坑与工程化改进方向
5.1 求解结果出现“既充电又放电”的奇怪现象
这个问题几乎每个做储能建模的人都会遇到。如果你没有加布尔互斥变量,直接把充放电功率限制为 0 ≤ P_ch ≤ P_ch_max、0 ≤ P_dis ≤ P_dis_max,优化器可能会让储能在同一时段同时充放电。原因很简单:充放电同时进行会产生额外损耗,等效于把电能“倒掉”,在目标函数里表现为增加了购电需求,按理不该发生。但如果你把储能损耗忽略掉,或者效率参数设成1,模型就可能在数值上出现无意义的进出。
解决办法就是我在2.3节里写的,用布尔变量把充放电状态互斥。如果不想引入整数变量,也可以用充放电效率非对称去天然惩罚同时性,但最稳妥的还是加互斥约束。
5.2 数值尺度差异导致的求解精度问题
这是另一个高频踩坑点。微电网里,储能容量动辄几千kWh,功率几百kW,而SOC又是个0到1之间的小数,三者数量级差异很大。如果你把储能容量单位设成Wh,SOC变量又是0.1到0.9,目标函数里的成本和这些变量一混合,求解器很容易出现精度问题,尤其在使用开源求解器时。
我的经验是:所有涉及能量的量,统一用kWh和kW;所有决策变量的数量级尽量控制在0.1到1000之间。 如果确实跨数量级,可以在求解前先做归一化,获取结果后再还原。
5.3 需求响应补偿系数怎么取才有意义
需求响应不是成本越低越好。如果补偿单价设置得比购电价还高,模型会倾向于不削负荷;设置得太低,模型又可能把可削减负荷全部用掉,对实际用户影响过大。
我建议把需求响应的补偿单价设置为“略低于高峰期购电价”的水平。具体做法是先跑一次无DR的调度,找到电网交互功率最高的时段和对应的边际电价,再用这个边际电价的一定比例(比如70%~80%)作为DR补偿单价,这样保证DR策略在需要的时候被调用,但不会被滥用。
5.4 从日前走向日内:滚动优化的进阶方向
严格来说,日前调度只是第一步。实际微电网运行中,光伏、风电的预测偏差在日内会暴露出来,这时候需要日内滚动优化来修正计划。标准做法是:每15分钟或1小时触发一次重新优化,只执行未来4小时内的计划,然后不断滚动。
这个想法可以基于本文的模型扩展,核心改动是把时间窗缩短、加入最新实测数据、把已经执行的调度置为已知量。我在工程项目的体会是:日前计划给方向,日内修正来解决不确定性,两者缺一不可。
5.5 求解器的选择建议
cvxpy 底层支持多个求解器,我的使用经验排序如下:
- Gurobi:工业级性能,处理几千个整数变量也不在话下,如果有教育或项目许可,优先选它
- CBC:开源里最稳的选择,能处理中小规模MILP
- GLPK_MI:轻量,适合跑通流程、教学验证,性能一般
- SCIP:对某些复杂MIP问题表现不错,但安装相对繁琐
我在文中的示例用了 GLPK_MI,因为它是很多Python环境里开箱即用的求解器。但如果你的模型规模扩大,比如96时段、多储能、多可控机组,我强烈建议换Gurobi或CBC,省下的调试时间远超换求解器的成本。
我自己在项目里还吃过一个亏:最开始用的开源求解器陷入迭代慢的问题,我一度以为模型写错了,花了两天查约束和变量,最后换求解器后秒解。所以遇到求解时间异常,先别怀疑模型,试试换求解器,往往立竿见影。
最后分享一个实操经验
这套代码跑通之后,我最大的收获不是“省了多少钱”,而是建立了一种把运行经验变成数学表达式的能力。你之前觉得“晚高峰放电、低谷充电、需求响应要适度”这些模糊的规则,在模型里不过是几条不等式和一个目标项。调度出来的方案可能和你的经验一致,但也有可能颠覆你的认知——比如某个时段储能既不充电也不放电,只是待机,因为充放损耗大于未来电价套利空间。
这个观察很关键。很多运行人员喜欢让储能“动起来”,总觉得储能闲着就是浪费。但从经济调度的角度看,设备不动作也是一种最优决策。 在做微电网调度系统时,一定要尊重模型给出的“不动作”结论,不要为了好看而强行加动作。
如果你刚开始接触微电网经济调度,我建议先用今天这份代码,用自己的负荷、光伏、风电数据跑一遍,从成本对比中找感觉,再逐步加需求响应、多储能、柴油发电机等复杂元素。代码地址和详细注释我都整理在对应项目里了,遇到问题随时交流。
