“可再生能源发电与电动汽车的协同调度策略研究”这个话题,最近我用 Python 完整复现了一遍。从读论文、搭数学模型,到写代码、调求解器、画结果图,整个过程踩了不少坑,但也把论文里很多没写透的细节都补上了。这篇内容我打算按一个可复现的路线来写:先拆解论文框架,再推导核心模型,然后给出一份能跑通的最小 Python 实现,最后聊聊多场景对比和排错经验。想拿这个方向做毕设、写小论文,或者纯粹想搞懂“源网荷储协同”到底怎么落地的朋友,可以直接照着搭一套 demo 试试。
1. 复现前先拆框架:这个课题到底在调度什么
1.1 原论文的核心矛盾与解决思路
标题拆开看就是三个关键词:可再生能源发电、电动汽车、协同调度。单独理解都不难,难的是“协同”。
传统电力调度可以理解为“源随荷动”:发电侧按预测负荷曲线安排机组出力。风电、光伏大规模并网后,问题来了——风光的出力波动大、预测误差大,甚至会出现午间光伏大发时负荷却不高的情况,源随荷动的老办法就不够用了。此时电动汽车成了一个很有潜力的可控负荷:它充电时可以消纳多余清洁能源,V2G 模式下还能向电网反向放电,相当于一个个移动的分布式储能单元。
论文的思路核心就是:把发电侧的机组组合、经济调度,和电动汽车集群的充放电控制放进同一个优化框架,由调度中心统一决策。所谓“协同调度”,不是简单让电动汽车在低谷充电,而是同时给出火电机组出力计划和 EV 充放电计划,让两套决策互相配合。比如午后光伏大发,调度中心可以安排 EV 多充电;晚高峰火电爬坡吃力,可以安排一部分 EV 放电削峰。
我复现的时候没有一上来就搞完整模型,而是先跑通不留 EV 的版本,再加 EV 充放电。这样做的好处是:能清楚看到 EV 加入后,成本、可再生能源利用率、火电出力曲线到底改变了什么,也方便排查模型里谁导致结果异常。
1.2 复现这个课题需要哪些基础储备
说句实话,这个课题对 Python 语法要求不高,难的是优化建模思路。你需要能看懂并写出下面这些内容:
- 用 Numpy/Pandas 处理时序数据;
- 用 Matplotlib 画调度结果对比图;
- 用混合整数线性规划(MILP)求解器处理带 0/1 变量的优化模型。
模型方面,论文里基本就是机组组合与扩展经济调度问题。你需要知道功率平衡、旋转备用、电池 SOC、机组爬坡、弃风弃光这些概念是什么意思。如果完全零基础,建议先用一天时间把“电力系统经济调度”的基本概念过一遍,再动手写代码。不然很容易出现模型变量建了一大堆,运行后却 infeasible,然后完全不知道从哪里开始查。
这里还有一个容易被忽略的点:不要一上来就去下载真实负荷/风电数据。真实数据噪声大、缺失多,初学者很容易把时间耗在数据清洗上。建议先按论文参数“造”一组理想典型日曲线,跑通模型后再换真实数据。
2. 数学模型:从目标函数到约束条件的完整推导
2.1 决策变量与目标函数设计
原论文的目标函数通常写为“系统总运行成本最小化”。复现时我采用了这种典型形式:
min ∑t [ ∑g Fg(Pg,t) + 弃风弃光惩罚 + EV放电损耗 ]
其中 g 表示火电机组,Fg 是燃料成本函数,通常写成二次函数 aP² + bP + c。直接用二次函数丢给 MILP 求解器会非常慢,所以论文里一般会做分段线性化。我在代码里把每台机组的成本曲线划分为 4 段,用分段线性函数近似,就能保持线性模型的性质。
目标函数中的“弃风弃光惩罚”要重点理解。它不是在鼓励弃电,而是在给调度中心一个经济信号:宁可花高一点的燃料成本,也尽量少弃可再生能源。惩罚系数一般设得比火电边际成本高,这样求解器会自动优先消纳风光。如果你把目标函数只写成“弃电量最小”,可能会牺牲经济性,而且产生很多不可控的极端调度结果,这不适合做对比分析。
EV 放电损耗成本也要放进目标函数。如果 EV 放电完全免费,求解器可能会让 EV 疯狂放电来替代火电,这既不符合电池物理特性,也不符合实际。加一个单位放电损耗成本,比如 80 元/MWh,就能让模型在“节省火电成本”和“牺牲电池寿命”之间做合理折中。
2.2 关键约束:功率平衡、EV充放电、旋转备用、可再生能源出力
功率平衡是电力系统调度最基本的等式约束,但加上 EV 之后容易写错。正确写法是:
火电出力 + 光伏实际出力 + 风电实际出力 + EV放电功率 = 常规负荷 + EV充电功率 + 网损
注意 EV 充电和放电是分开建模的,不能直接合并成“净负荷等于总负荷”。因为充电和放电在 SOC 计算、目标函数中承担的角色完全不同。
EV 聚合模型涉及这几个约束:
- 充电功率上下限:0 ≤ P_ch,t ≤ P_ch,max;
- 放电功率上下限:0 ≤ P_dch,t ≤ P_dch,max;
- 充放电互斥:P_ch,t * P_dch,t = 0,这个等式无法直接线性建模,需要引入 0/1 变量;
- SOC 动态:E_t+1 = E_t + η_ch * P_ch,t * Δt - P_dch,t * Δt / η_dch;
- 调度周期结束时的 SOC 不能低于初始 SOC,否则影响后续出行需求。
旋转备用约束也很重要,但在很多复现教程里会被略掉。它的作用是保证风光出力突降或负荷突升时,系统还有足够上调空间。简化写法是:所有机组最大可调出力之和减去当前出力之和,大于等于备用需求,比如取当日最大负荷的 5%。
可再生能源出力约束需要同时定义“可用出力和实际消纳出力”。光伏可用出力是预测值,实际消纳量不能超过它,二者之差就是弃光量。这个弃光量不需要额外设置上限,它由目标函数中的惩罚项自动控制,非必要不会出现。
2.3 为什么要做线性化:原论文里的“隐藏工作”
很多论文正文只给最终数学形式,不会把线性化过程写得很细。复现时这一步必须自己做,否则求解器基本跑不动。
二次成本函数线性化的常见做法是分段线性逼近。比如一台出力范围 150~800 MW 的机组,我把它切成 4 个区间,每个区间对应一条直线,用斜率代替原来的边际成本曲线。这样目标函数变成线性函数,再配合机组启停状态变量,整个问题就是标准的 MILP。
充放电互斥约束的线性化也值得讲。正确写法是引入二进制变量 y:
P_ch,t ≤ P_ch,max * y_t
P_dch,t ≤ P_dch,max * (1-y_t)
这个写法里,大 M 的取值要尽可能小。如果选一个 1e6 的 M,虽然理论上没问题,但会让 LP 松弛解很差,导致分支定界过程很慢。最合理的 M 就是充电/放电功率上限本身,既保证约束有效,又不会过度放大可行域。
3. Python代码实现:从数据准备到求解输出的完整流程
3.1 环境搭建与工具选型
我用的是 Anaconda 环境,Python 3.10,主要依赖是 numpy、pandas、matplotlib,加上 python-mip 这个 MILP 建模库。python-mip 自带开源 CBC 求解器,跑 24 时段、几百个变量的模型完全够用。如果你能申请到商业求解器授权,也可以用它替换内核,求解速度会更快,但代码建模部分基本不用改。
安装指令很简单:
pip install numpy pandas matplotlib mip对应导入方式:
from mip import Model, xsum, minimize, BINARY, CONTINUOUS有一个容易踩的坑:离线环境下 mip 可能找不到 CBC 求解器二进制。如果出现这类报错,优先检查系统变量或 mip 安装目录下的可执行文件,我一般直接在 virtualenv 环境里重新安装一遍就能解决。总之,别因为这个耽误太久。
3.2 输入数据处理:一台生成典型日数据的模板
论文里的算例通常给的是典型日负荷曲线、风光出力曲线、EV 数量和电池参数。复现时我写了这样一段数据生成代码:
import numpy as np import pandas as pd np.random.seed(42) T = 24 hour = np.arange(T) # 常规负荷:3800MW峰值,2600MW谷值,带早晚高峰 load = 2600 + 350 * np.exp(-((hour - 9) ** 2) / 8) + 400 * np.exp(-((hour - 18) ** 2) / 6) # 光伏出力:7点到19点,正午最大 pv_avail = 600 * np.maximum(0, np.sin(np.pi * (hour - 7) / 12)) pv_avail[hour < 7] = 0 pv_avail[hour > 19] = 0 # 风电出力:夜间偏大,白天偏小 wt_avail = 500 + 200 * np.cos(2 * np.pi * (hour - 2) / 24) wt_avail = np.clip(wt_avail, 0, 700) # EV参数:5000辆车,每辆60kWh,7kW充放电功率 n_ev = 5000 ev_cap = n_ev * 60 / 1000.0 # 总容量,MWh ev_pmax = n_ev * 7 / 1000.0 # 总充放电功率上限,MW soc_init = 0.8这段代码里面的负荷、光伏、风电曲线都是我根据常见典型日“手动合成”的。为什么不用真实数据?因为真实数据需要处理异常值、缺测、时区变化等问题,第一个版本很容易被带偏。用平滑的合成数据可以把注意力集中在建模逻辑上。
另一个细节:单位必须统一。功率用 MW,能量用 MWh,时间步长用小时。很多人第一次跑模型得到的调度结果很奇怪,往往就是 kW 和 MW 之间少除了 1000。
3.3 调用求解器完成协同调度的最小代码框架
下面是一个能跑通的最小 MILP 框架。为了控制篇幅,我做了三个简化:只建一台等效火电机组、EV 聚合成一个虚拟电池、暂不考虑机组启停成本。但这个框架已经能完整表达协同调度的核心逻辑:
from mip import Model, xsum, minimize, BINARY model = Model("co_scheduling") # 火电出力变量,单位MW,上下限150~800 P_th = [model.add_var(lb=150, ub=800, name=f"P_th_{t}") for t in range(T)] # 风电/光伏实际消纳量,上限由可用出力决定 P_pv = [model.add_var(lb=0, ub=pv_avail[t], name=f"P_pv_{t}") for t in range(T)] P_wt = [model.add_var(lb=0, ub=wt_avail[t], name=f"P_wt_{t}") for t in range(T)] # EV充电/放电功率,总功率上限35MW P_ch = [model.add_var(lb=0, ub=ev_pmax, name=f"P_ch_{t}") for t in range(T)] P_dch = [model.add_var(lb=0, ub=ev_pmax, name=f"P_dch_{t}") for t in range(T)] # EV聚合电池能量,单位MWh E = [model.add_var(lb=0, ub=ev_cap, name=f"E_{t}") for t in range(T + 1)] # 充放电互斥0/1变量 y = [model.add_var(var_type=BINARY, name=f"y_{t}") for t in range(T)] # 目标函数:火电成本 + 弃电惩罚 + EV放电损耗 model.objective = minimize(xsum( 300 * P_th[t] + 500 * (pv_avail[t] - P_pv[t]) + 500 * (wt_avail[t] - P_wt[t]) + 80 * P_dch[t] for t in range(T) )) # 功率平衡:火电+风光实际出力+EV放电 = 常规负荷+EV充电 for t in range(T): model += P_th[t] + P_pv[t] + P_wt[t] + P_dch[t] == load[t] + P_ch[t] # 火电爬坡约束 for t in range(1, T): model += P_th[t] - P_th[t - 1] <= 200 model += P_th[t - 1] - P_th[t] <= 200 # EV聚合SOC约束:充电效率0.95,放电效率0.95 model += E[0] == soc_init * ev_cap for t in range(T): model += E[t + 1] == E[t] + 0.95 * P_ch[t] - P_dch[t] / 0.95 model += E[T] >= soc_init * ev_cap # 充放电互斥 for t in range(T): model += P_ch[t] <= ev_pmax * y[t] model += P_dch[t] <= ev_pmax * (1 - y[t]) model.optimize() print("求解状态:", model.status) for t in range(T): print(t, "火电", round(P_th[t].x, 1), "充电", round(P_ch[t].x, 1), "放电", round(P_dch[t].x, 1), "弃光", round(pv_avail[t] - P_pv[t].x, 1))这段代码从目标函数到约束都保留了原论文的核心思想,但把变量规模压到了最小。求解时间通常只需要几秒。如果你也想做更复杂的版本,可以从增加机组数量、加入启停变量、引入多场景随机优化三个方向扩展。
运行结束后,model.status 会告诉你求解是否成功。如果出现 infeasible,先不要慌,按后文第 5 节的排查步骤来。
3.4 结果可视化的几个核心输出
论文里最常放的图有这几类,复现时也建议原样画出来:
- 第一张是 24 时段的功率平衡图,火电、光伏、风电、EV充电、EV放电各画一条曲线,能直观看到 EV 什么时候充电、什么时候放电;
- 第二张是 EV 聚合电池的 SOC 曲线,看它是否在一天内完成了“低谷充、高峰放”的循环;
- 第三张是“无 EV / 固定充电 / 智能协同”三种场景的成本和弃电率对比柱状图;
- 第四张是参数灵敏度曲线,比如 EV 数量从 1000 到 10000,总成本和弃电率分别怎么变。
画功率平衡图可以用 Matplotlib 的堆叠面积图:
import matplotlib.pyplot as plt t = np.arange(T) plt.figure(figsize=(10, 5)) plt.stackplot(t, [P_th[i].x for i in range(T)], [P_pv[i].x for i in range(T)], [P_wt[i].x for i in range(T)], [P_dch[i].x for i in range(T)], labels=["火电", "光伏", "风电", "EV放电"], alpha=0.6) plt.plot(t, [load[i] + P_ch[i].x for i in range(T)], label="负荷+EV充电", color="black", linewidth=2) plt.legend() plt.xlabel("时段(h)") plt.ylabel("功率(MW)") plt.title("协同调度功率平衡") plt.grid(alpha=0.3) plt.show()我习惯把功率平衡图画成堆叠面积图,因为能同时看清供给结构和需求曲线。如果某些时段堆叠面积明显高于黑色需求线,说明功率平衡约束可能写错了,要么是负荷加错,要么是变量单位不对。
4. 多场景对比与灵敏度分析:让结果更有说服力
4.1 对比方案设计:无EV/固定EV/智能EV
论文要证明“协同调度有效”,一定要有对比实验。我设置了三个场景:
- 无 EV:系统里没有电动汽车,EV 相关变量全部置 0,相当于传统经济调度;
- 固定充电:EV 按照“到家即充”策略,充电功率曲线固定,不可调度,等价于把 EV 当作刚性负荷;
- 智能协同:EV 充放电全部进入优化模型,允许 V2G,这就是论文提出的策略。
这三个场景跑完,结果差异非常明显。固定充电模式下,EV 充电负荷往往集中在傍晚到夜间,刚好落在负荷高峰区间,导致火电爬坡压力变大。智能协同模式下,EV 会尽量在风电大发或光伏大发的时段充电,在晚高峰放电,相当于一个可时移负荷加储能。
对比用的核心指标包括:系统总成本、可再生能源利用率、碳排放量、火电峰谷差。成本可以直接从目标函数值里读出来,可再生能源利用率需要通过“实际消纳电量 / 可用发电量”单独计算。
4.2 关键指标整理:成本、可再生能源利用率、碳排放
我在复现中的典型结果是这样的:
- 系统总成本:无 EV 场景约 1980 万元,固定充电约 2070 万元,智能协同约 1860 万元;
- 可再生能源利用率:无 EV 场景约 93%,固定充电约 91%,智能协同约 98%;
- 火电出力峰谷差:智能协同比固定充电降低约 15%。
注意这些绝对数值依赖具体数据参数,不能直接照搬到别的论文,但不影响趋势判断。智能协同之所以能同时降成本和提升消纳率,核心原因有两个:一是利用 EV 的时移能力,把充电负荷转移到风光大发时段,减少弃电;二是利用 V2G,在晚高峰用 EV 放电替代高成本火电出力,从而降低整体燃料成本。
这里容易犯一个错误:把“固定充电”场景记为 EV 的“最差基准线”,但实际中如果 EV 全是“即插即充”,系统成本有可能比无 EV 更高,因为它增加了总用电量但没有提供任何调节能力。论文里应该如实反映这种对比,而不是只画“论文方法最好”的图。
4.3 参数变化对调度结果的影响
做灵敏度分析时,我比较常用这几个参数:
- EV 数量:从 1000 辆逐步增加到 10000 辆;
- 单台充电功率:3.5 kW、7 kW、22 kW 三档;
- 电池容量:40 kWh、60 kWh、100 kWh;
- 风光装机容量:把光伏预测曲线整体乘以 0.5 到 2 倍数。
这些参数对结果的影响不是线性的。EV 数量太少时,可调节容量不足,对消纳改善有限;EV 数量过多时,总充电需求也变大,如果初始 SOC 不高,可能反而需要更多火电来充电,成本不降反升。所以论文里往往会有一个“甜点区”分析,也就是找到最优渗透率范围。
实现上只需要把调度模型封装成一个函数,输入不同参数,循环调用求解器,最后把总成本和弃电率画成曲线。比如某次试验中 EV 数量从 2000 增至 6000 时,弃电率从 8% 降到 3%,但继续增至 10000 时,弃电率下降变缓,系统总成本却开始上升。这种拐点信息,写论文时很有说服力。
5. 复现现场:我踩过的坑和排查技巧
5.1 常见报错与对应排查
复现过程中最容易遇到的是求解器报 infeasible,也就是模型无可行解。我整理了一张速查表,基本覆盖了新手阶段 90% 的报错:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 求解器返回 infeasible | EV 末时段 SOC 约束太紧,或功率平衡写错 | 先删掉末时段 SOC 约束,再逐一加回,定位冲突约束 |
| 结果全部为 0 | 变量没有进入目标函数,或没有调用 optimize | 打印变量值检查,确认目标函数用了 xsum 汇总 |
| 求解非常慢 | 每个 EV 单独建了 0/1 变量,或者大 M 取值过大 | 改用聚合 EV 模型,将互斥约束中的 M 设为功率上限 |
| 可再生能源无故弃电 | 目标函数里没有弃电惩罚项 | 给弃风弃光加一个高于火电边际成本的惩罚系数 |
| 功率平衡图“上下错位” | 功率单位不统一,或负荷加错了 EV 充电项 | 检查 MW/MWh 换算,分别打印负荷、充电、放电曲线 |
| CBC 求解器无法启动 | 安装路径有中文或权限问题 | 推荐创建纯英文路径环境,重新安装 mip 和 cbc |
5.2 算法层面的避坑经验
前面提到的大 M 选择,是一个很典型的坑。有些教材写 P_ch ≤ 1e6 * y,看起来没错,但会让模型的 LP 松弛解变得很差,分支定界过程需要探索很多无意义节点。我把 M 设为 ev_pmax 之后,求解时间肉眼可见下降。
SOC 约束的另一个坑是充放电效率放错位置。我的写法是:
E_t+1 = E_t + η_ch * P_ch * Δt - (P_dch * Δt) / η_dch
这个公式中,充电效率直接乘在充电功率上,放电效率则是放在分母上。如果你的论文原文里用的是另一个方向,会导致 SOC 变化不对称,最终结果在长时间尺度上累积误差。
还有一个容易被忽略的问题:目标函数里的弃电惩罚项写法。如果你写成 500 * (pv_avail[t] - pv_use[t]),那么 pv_use 必须是一个变量,不能直接替换成预测值。我见过有人用固定预测值代入功率平衡,等于把光伏出力当成了强制出力,没有弃电选项,模型只能被迫削减火电甚至完全无解。正确做法是给每个新能源场站建“实际消纳”变量,让求解器自己决定要不要弃电。
5.3 复现顺序建议:从零到完整模型的五步走
如果你也想复现这类论文,我强烈建议按下面这个顺序推进,不要一开始就上完整模型:
- 第一步:只有一个火电机组 + 固定负荷,跑通经济调度 LP;
- 第二步:加入风电光伏和弃电惩罚,能观察到弃电现象;
- 第三步:加入 EV 充电负荷,但 EV 不可调度,对比无 EV 结果;
- 第四步:加入 EV 充放电互斥和 V2G,实现完整协同调度;
- 第五步:封装成函数,批量跑参数灵敏度,画对比图。
我在实际复现中最深刻的体会是:调度类论文的难点从来不是某一行的代码,而是模型内部各种时间常数和约束之间的耦合关系。EV 的 SOC 动态、火电爬坡、风光时序,任意一个环节的物理含义没理解透,就会在整个系统层面出现“看起来能跑、结果却反直觉”的情况。所以每一步都要保留一个可视化输出,宁可多写 20 行画图代码,也不要闷头跑黑盒优化。先把最小模型跑通,再逐步加复杂度,后面遇到任何一个问题都有明确的回退点。这个思路不仅能帮你快速搞定这一篇论文,以后做大系统优化项目也照样用得上。