☰
可再生能源与电动汽车协同调度:基于Pyomo的建模与求解实践
2026/10/22 19:58:22 网站建设 项目流程

1. 从论文复现到工程落地:为什么非要“协同调度”

硕士论文复现这件事,做过的都知道,最难受的不是代码跑不通,而是论文里的公式明明写得清清楚楚,你却不知道怎么把它变成一行行能运行的 Python。我一开始接触“可再生能源发电与电动汽车的协同调度策略研究”这个题目时,第一反应是“这不就是把风电、光伏和充电桩放一起优化么”,等到真正动手才发现,坑远比想象的多。

先说这个课题到底在解决什么问题。常规电力系统里,火电是主力,风电光伏是“看天吃饭”的波动电源,电动汽车是无序负荷——大家各干各的。火电为了配合风光出力,必须频繁升降负荷,机组磨损大、煤耗高;电动汽车集中在傍晚无序充电,又和晚间用电高峰撞在一起。协同调度的核心逻辑,就是利用电动汽车的“可调控性”,把一部分充电负荷转移到风光大发时段,同时让火电出力曲线更平滑。说白了,就是用时间换空间,用调度换经济性。

这个方向目前已经不光是学术圈的宠儿,很多实际项目里已经在落地。比如某些园区微网,白天光伏大发时给电动车充电,晚上电动车反过来给办公楼供电,这就是协同调度的雏形。所以这篇论文复现的价值,不只是为了毕业,而是把一个可以用在真实场景里的优化问题,完整地从建模到代码走一遍。

这篇文章适合两类人:一类是正在做相关课题、需要参考代码和建模思路的学生,另一类是搞过电力系统优化、但对电动汽车聚合建模不熟的工程师。我会把整个复现过程拆开,从数学模型的建立,到 Python 代码的架构,再到求解过程中踩过的坑,一步步说清楚。后面所有代码片段,都是我在复现过程中实测跑通的,直接拿来改就能用。

2. 建模之前的灵魂拷问:协同调度的数学本质是什么

2.1 为什么“协同”而不是“各干各的”

在做任何代码之前,先想明白一个事情:协同调度的“协同”到底体现在哪。电力系统调度本质是一个多时段决策问题,典型的时间尺度是 24 小时,步长取 1 小时。每个时段,系统要决策火电出多少、风光弃多少、电动汽车充多少。如果没有协同,那每个决策是独立的——火电按负荷曲线走,电动汽车按用户习惯走。但加了一个目标函数把它们串起来之后,每个决策都会影响其他决策,这就是协同。

我用一个简单例子来说明。早上 10 点到下午 2 点,光伏大发,系统净负荷下降,火电为了不深度调峰,有两种选择:一是弃掉一部分光伏,二是把电动汽车的充电负荷引导到这个时候。第一种选择损失的是清洁能源,第二种选择让电动汽车用户享受了低价电,两边都受益。协同调度的本质,就是在这个多时段、多主体的可行域里,找到让总成本最小的那个“平衡点”。

所以建模的第一步,不是写代码,而是把决策变量、目标函数、约束条件一件件理清楚。直觉上,你可能会把火电成本、弃风弃光惩罚、电动汽车充电费用全部加在一起作为目标函数。但这里有个关键细节:电动汽车的充电费用是用户付的,火电成本是电网公司付的,两者“钱袋子”不一样。论文里通常把它处理成社会总成本最小,这在理论上没问题,实操时要注意成本系数的量纲一致性,否则优化结果会一边倒。

2.2 目标函数与约束条件:每一行数学都要能对上代码

我在复现时,目标函数用的是最经典的形式:火电运行成本加弃风弃光惩罚加电动汽车购电成本。火电成本近似为出力的二次函数,但实际建模时用分段线性化处理,因为主流混合整数线性规划求解器对线性约束的支持比非线性好得多。

具体来说,目标函数长这样:

[ \min \sum_{t=1}^{T} \left[ \sum_{i \in G} (a_i P_{i,t}^2 + b_i P_{i,t} + c_i) + C_{curtail} \cdot (P_{curtail,t}) + \sum_{j \in EV} \rho_t P_{j,t}^{ch} \right] ]

这里 ( G ) 是火电机组集合,( EV ) 是电动汽车集合,( P_{curtail,t} ) 是 t 时段总弃风弃光电量,( \rho_t ) 是 t 时段电价,( P_{j,t}^{ch} ) 是第 j 辆电动车 t 时段的充电功率。

注意,二次成本在代码里要么用分段线性近似(Piecewise Linear),要么用二次规划求解器。我复现的论文用的是混合整数线性规划,所以我把火电成本做成了三段分段线性函数,误差控制在 1% 以内。这里有个经验:分段数太少,优化结果会“钻空子”,比如跳过分段边界来回振荡;分段数太多,求解速度会显著下降。对 24 时段、4 台火电、500 辆电动车这样的规模,三段足够了。

约束条件才是真正麻烦的地方。我整理了一个清单,基本涵盖了论文里会出现的所有约束:

  • 功率平衡约束:每时段火电出力 + 风光出力 + 电动汽车放电(如果有 V2G) = 基础负荷 + 电动汽车充电负荷。
  • 火电出力上下限及爬坡约束:相邻时段出力变化不能超过爬坡速率,这是保证机组物理可行性的关键。
  • 电动汽车充电需求约束:全天充电总量必须满足用户设定的里程需求,这是“公平性”约束,不能为了削峰把电动车给饿着。
  • 充电功率上下限约束:受充电桩额定功率和电池最大允许充电功率限制。
  • 风光出力上限约束:弃风弃光量非负且不能超过当前可再生出力。

在代码里,这些约束直接用优化建模库的 Constraint 对象表达。我最开始用 PuLP,后来换成 Pyomo,因为 Pyomo 对分段线性函数的支持更友好,而且可以无缝切换求解器。

2.3 风光不确定性:为什么简单场景也要引入场景法

很多复现论文的第一版都不会考虑不确定性——假设风光出力是确定值,按预测曲线走。但这个硕士论文原题里专门研究了不确定性,如果你跳过去,后面跟导师汇报时容易被问住。

处理不确定性常见有三类方法:鲁棒优化、随机规划、模型预测控制。论文里用的是随机规划里的“场景法”——生成若干组风光出力场景,每组场景对应一个概率,目标函数变成所有场景下成本的期望值。这样问题规模会成倍增加:本来 24 时段的问题,加了 10 个场景就变成 240 个时段的决策,但好处是结果更贴近真实运行情况。

我在复现时没有自己做场景生成,而是直接用了论文的附录数据——通过拉丁超立方采样生成 20 组场景。实际跑下来发现一个问题:场景数太少,结果会被个别极端场景主导;场景数太多,求解时间从几秒涨到几分钟。20 组算是一个性价比比较高的折中,这也是论文里没明说、但实操中很关键的一个参数。

3. 数据准备与仿真场景构建:一半时间都在和“脏数据”搏斗

3.1 负荷曲线和风电光伏出力的时间序列怎么来

复现论文时最尴尬的情况是:论文里啥数据都有,就是不给原始数据文件。好在大多数论文会给出关键曲线的形状特征,比如“冬季典型日负荷曲线”“夏季光伏出力曲线”,这些描述足以让我自己造出风格一致的数据。

基本的做法是用几段正弦曲线叠加,再加点随机波动。比如基础负荷,我会设定一个 1000MW 到 1600MW 之间的范围,早晚两个峰,凌晨一个谷。风电出力则是夜间大、白天小,用“夜间抬升 + 白天降低”的曲线;光伏则是标准的钟形曲线,中午 12 点到下午 2 点达到峰值。

我强烈建议用 Python 的 NumPy 生成这些曲线,然后用 pandas 存成 DataFrame,因为后续所有代码都要基于时间索引来操作。这里贴一段我实际用的数据生成代码:

import numpy as np import pandas as pd np.random.seed(42) T = 24 # 24小时 t = np.arange(T) # 基础负荷:早晚双峰曲线 base_load = 1200 + 150 * np.sin(2 * np.pi * (t - 8) / 24) + 200 * np.exp(-((t - 19) ** 2) / 8) base_load += np.random.normal(0, 15, T) # 光伏出力:钟形曲线 pv_output = 400 * np.exp(-((t - 12.5) ** 2) / 12) pv_output = np.clip(pv_output, 0, None) # 风电出力:夜间大、白天小 wind_output = 300 + 180 * np.cos(2 * np.pi * (t - 2) / 24) wind_output += np.random.normal(0, 20, T) wind_output = np.clip(wind_output, 80, 500) df = pd.DataFrame({ 'hour': t, 'load': base_load, 'pv': pv_output, 'wind': wind_output })

代码里几个参数值得解释。np.random.seed(42)固定随机种子,这是复现实验必需的操作,否则每次跑结果都不一样。np.clip把光伏低于 0、风电超上限的值都截断掉,虽然简单粗暴,但能避免求解器遇到物理上不可能的负出力。

3.2 电动汽车集群参数:单辆车太细,必须聚合建模

电动汽车的问题如果不做聚合,建模会非常痛苦。假设系统里有 2000 辆电动车,每辆车都有不同的初始 SOC、充电功率上限、接入和离开时间,如果一辆车建一组变量,模型规模会爆炸。论文里用的是“集群聚合”的思路:把同类型电动车合并成一个集群,用集群总容量和总功率上限来描述。

我复现时设置了三个集群:白天上班人群(早 8 点接入,下午 5 点离开)、晚间通勤人群(下午 6 点接入,次日早 7 点离开)、营运车辆(如网约车,几乎全天接入但单次充电需求高)。每个集群只有 5 个参数:车辆数、单辆车电池容量、平均充电功率上限、接入时段集合、离网时需满足的总 SOC 目标。

这里有个小细节:集群的充电总功率上限不等于车辆数乘以单桩功率,因为同一时刻不一定所有车都插着枪,所以我在代码里加了一个“同时率”系数,取 0.8。这个系数论文里不会写,但实际调度系统里必须考虑,否则优化出来的充电计划在物理上根本执行不了。

3.3 场景生成与参数敏感性:复现结果对参数到底有多敏感

场景生成我用了蒙特卡洛抽样——对预测风光出力叠加不同大小的高斯扰动。对每个时段,生成 20 个随机扰动,就得到 20 个场景。每个场景权重取 1/20。严格来说,更专业的做法是先用历史数据拟合预测误差分布,再采样,但论文复现阶段用高斯分布足够了。

跑完第一次优化后,我建议做一轮敏感性分析:把火电成本系数上下浮动 10%,看充电计划变化大不大;把电动汽车规模翻一倍,看系统是优先多建充电桩还是多建储能。这种分析不是论文验证的必要步骤,但在答辩时非常加分,而且能帮你发现模型里哪些参数是“命门”。

我做的敏感性分析结果很能说明问题:对总成本影响最大的是火电爬坡约束的松紧,其次是电动汽车同时率系数,风光预测误差反而没那么敏感。这说明系统的灵活性瓶颈主要在火电,不在新能源。这个结论放到实际运行中也成立——很多电网调度难,不是因为风光太多,而是火电跟不上。

4. 代码架构与核心实现:从建模到求解,一步步拆给你看

4.1 为什么选 Pyomo + Gurobi:求解器选型背后的逻辑

Python 里做优化建模,主流选择有 PuLP、Pyomo、Gurobipy、OR-Tools。我最终的组合是 Pyomo 建模 + Gurobi 求解。选 Pyomo 的原因很实际:论文复现可能需要换求解器验证结果,Pyomo 的抽象模型语法让我可以在 GLPK、CBC、Gurobi 之间无缝切换,而不改模型代码。Gurobi 是商业求解器,对学生免费,求解速度和稳定性都比开源求解器好一个数量级。

如果你没有 Gurobi 的许可证,用开源的 CBC 也能跑,只是 20 场景的问题可能要等几分钟,而 Gurobi 只需要十几秒。差距主要在混合整数变量的分支定界效率上。

举个具体的例子。火电的最小启停时间约束,如果建模成整数变量,CBC 可能要探索几万个节点,Gurobi 可能几千个节点就找到最优解了。对于硕士论文的工作量,时间就是效率,能用好工具就别折磨自己。

4.2 模型定义:一个完整的 Pyomo 调度模型骨架

下面这个代码块是我复现时用的模型骨架,去掉了论文里部分复杂约束,保留了核心逻辑。你可以直接复制到一个 Python 文件里跑。

import pyomo.environ as pyo # 数据准备 T = 24 G = range(4) # 4台火电机组 EV = ['cluster1', 'cluster2', 'cluster3'] # 假设从DataFrame中提取数据 load = df['load'].values pv = df['pv'].values wind = df['wind'].values model = pyo.ConcreteModel() # 时段集合 model.T = pyo.Set(initialize=range(T)) model.G = pyo.Set(initialize=G) model.V = pyo.Set(initialize=EV) # 决策变量 model.pg = pyo.Var(model.G, model.T, within=pyo.NonNegReals) # 火电出力 model.pch = pyo.Var(model.V, model.T, within=pyo.NonNegReals) # EV充电功率 model.pcurtail = pyo.Var(model.T, within=pyo.NonNegReals) # 弃风弃光 # 目标函数:火电煤耗成本 + 弃电惩罚 + EV充电费用 def objective_rule(m): cost_fuel = sum(3.2 * m.pg[g, t] + 0.02 * m.pg[g, t] ** 2 for g in m.G for t in m.T) cost_curtail = sum(50 * m.pcurtail[t] for t in m.T) cost_ev = sum(0.8 * m.pch[v, t] for v in m.V for t in m.T) return cost_fuel + cost_curtail + cost_ev model.obj = pyo.Objective(rule=objective_rule, sense=pyo.minimize) # 功率平衡约束 def power_balance(m, t): return sum(m.pg[g, t] for g in m.G) + pv[t] + wind[t] - m.pcurtail[t] \ == load[t] + sum(m.pch[v, t] for v in m.V) model.balance = pyo.Constraint(model.T, rule=power_balance) # 火电出力上下限 model.limit_up = pyo.Constraint(model.G, model.T, rule=lambda m, g, t: m.pg[g, t] <= 500) model.limit_down = pyo.Constraint(model.G, model.T, rule=lambda m, g, t: m.pg[g, t] >= 50) # EV充电日总量约束(简化:每个集群总充电量达到目标) total_charge_target = {'cluster1': 800, 'cluster2': 1200, 'cluster3': 600} def ev_total(m, v): return sum(m.pch[v, t] for t in m.T) >= total_charge_target[v] model.ev_target = pyo.Constraint(model.V, rule=ev_total) # 求解 solver = pyo.SolverFactory('gurobi') solver.options['mipgap'] = 0.01 result = solver.solve(model, tee=True)

注意火电成本我用的是“3.2 * P + 0.02 * P^2”,这个二次项在 Pyomo 里会被当作非线性问题处理。如果要走 MILP,必须把二次项改成分段线性。我这里为了代码可读性保留了二次形式,实际论文复现时我是用 Pyomo 的Piecewise组件来处理的。下面这一小段是分段线性的实现方法:

from pyomo.core import Piecewise # 对每个机组每个时段,把二次成本线性化 # 简化的写法:定义3段线性 breakpoints = [0, 150, 300, 500] f_vals = [0, 3.2*150 + 0.02*150**2, 3.2*300 + 0.02*300**2, 3.2*500 + 0.02*500**2] model.pg_cost = Piecewise(model.G, model.T, model.pg, pw_pts=breakpoints, pw_constr_type='EQ', f_rule=lambda m, p: np.interp(p, breakpoints, f_vals))

贴上这两段代码,我想强调一个点:Pyomo 的Piecewise用起来不难,但坑在于pw_constr_type选'EQ'还是'LB'。如果你的目标是最小化成本,'EQ'会引入额外的二进制变量,'LB'就不需要,求解更快。代价是'LB'在某些情况下会让最终解落在分段函数的低估区,需要额外校验。我第一次用'EQ'导致整数变量翻倍,求解时间从 20 秒涨到 5 分钟,后来改成'LB'就好了。

4.3 结果分析与可视化:调度曲线不会说谎

求解结束之后,最要紧的是画图。我通常画三张图:第一张是系统功率平衡图,显示每个时段的负荷、火电、风光、电动汽车充电功率,能直观看出“削峰填谷”有没有成功;第二张是火电出力曲线和爬坡率曲线,看火电是不是被调得太累;第三张是电动汽车集群的充电计划甘特图,看每个集群在哪些时段充电。

用 matplotlib 画图时有一个细节:时间轴一定要标清楚,最好用“0时—23时”而不是“1时—24时”,否则和论文里的日负荷曲线对不上。另外,弃风弃光的那段曲线要单独用阴影标注,这是协同调度效果最直接的可视化证据。

这里有个经验:如果你发现优化结果里某个时段火电爬坡率正好顶在约束边界上,不要慌,这是正常的,说明模型“物尽其用”。如果所有时段爬坡率都离边界很远,那说明要么爬坡约束设置太宽松,要么目标函数权重不合理,需要回头检查参数。

5. 求解过程中踩过的坑与速查清单

5.1 模型不可行的优先级排查法:从约束冲突中找到元凶

模型不可行是优化问题最经典的头疼事,特别是约束一大,你根本不知道是哪个约束在打架。我在复现时遇到过三次不可行,每次原因都不一样,这里把排查方法分享出来。

第一步,检查功率平衡约束。把风光、负荷、EV 数据代入,算一下每个时段“最大可发功率”是否大于“最小需求功率”。如果某个时段最大可发功率都小于最小需求,那必不可行。这个可以直接用 pandas 算,几秒钟就能定位。

第二步,检查 EV 充电目标是否过高。total_charge_target设置成 1200MWh,但所有 EV 集群总功率上限 × 接入时段最大只有 1000MWh,那一定不可行。换算成行话:充电需求总量必须小于等于充电能力总量。

第三步,用 Gurobi 的计算不可行度功能(IIS)定位冲突约束集合。Pyomo 里可以这样调用:

solver = pyo.SolverFactory('gurobi') res = solver.solve(model, tee=True, report_timing=True, symbolic_solver_labels=True) # 获取IIS solver.options['iis'] = 1 res = solver.solve(model, tee=True)

Gurobi 会把不可行的约束集合列出来,通常是一小组约束,直接看它们之间的逻辑冲突就能找到问题。这个工具比人肉排查快得多,强烈建议用。

5.2 求解时间爆炸:二进制变量是罪魁祸首

MILP 的求解时间对二进制变量数量特别敏感。EV 充放电变量如果允许双向(充和放分开建模),二进制变量数量直接翻倍。我在第一版模型里给每个 EV 集群的每个时段都加了“是否充电”的开关变量z[v,t],再加上“是否放电”的开关变量,结果 20 个场景下模型有 24 × 3 × 2 × 20 = 2880 个二进制变量,求解时间冲到 15 分钟。

后来我做了两个简化:一是去掉放电选项,因为原论文里 V2G 只作为扩展场景讨论;二是把“必须充满”的硬约束改成“允许 5% 偏差”的软约束,用罚函数项处理。改完之后二进制变量降为 1440 个,求解时间控制在 40 秒内。

这里有个方法论上的教训:论文复现不是把论文的每个细节都搬到代码里,而是要理解哪些细节对结果机制有决定性影响,哪些只是点缀。V2G 在原论文里是重要创新点,但在基础版本里完全可以先砍掉,跑通之后再逐步加回来。

5.3 结果“不美观”:优化结果不符合直觉怎么办

第一次跑完,我发现充电计划全挤在凌晨 3 点到 5 点,虽然成本最低,但现实里这时候充电站没人、车也少,根本不可能执行。这就是纯优化模型的通病——只认成本,不认现实约束。

解决方法是在模型里加“电动汽车用户舒适度约束”,比如限制单个时段充电功率不能超过集群总需求的 30%,或者规定白天 8 点到 20 点之间必须完成至少 40% 的充电量。这些约束不是从物理公式推出来的,而是从用户行为习惯总结的,论文里叫“行为约束”。加上之后,充电计划明显更符合实际,虽然总成本上升了 6%,但结果的可解释性提高了非常多。

我应该早点说这个事:复现论文最容易被忽略的环节,就是“结果合理性校验”。数值最优不代表方案可用,一定要用常识去审视每一条调度曲线。我在给某导师汇报的时候,对方第一个问题就是“凌晨三点让几百辆网约车去充电,你考虑过司机在睡觉吗?”——这种问题只能用行为约束来回答。

5.4 FAQ 速查表

现象可能原因处理办法
模型不可行功率平衡约束无解检查各时段最大发电能力是否小于最小负荷
求解时间过长二进制变量太多去掉非必要整数变量,使用软约束 + 罚函数
充电计划集中凌晨缺少用户行为约束加入时段充电比例约束
弃风弃光量异常大火电最小出力限制太高降低机组最小技术出力或允许深度调峰
两次运行结果不一致随机场景未固定种子固定np.random.seed,并保存场景数据文件
Gurobi 求解器报许可证错误License 环境变量未配置在代码开头设置GRB_LICENSE_FILE路径

这些坑每一个我都亲自踩过。最浪费时间的是第三项,因为模型没有“报错”,只是默默给了一个数学上最优、现实中无用的解。如果你也在复现这类调度论文,强烈建议在跑优化之前先列出“结果合理性检查单”:充电是否集中在合理时段、火电是否频繁启停、弃电比例是否在可接受范围。这三条过了,基本可以保证优化结果经得起推敲。

6. 个人体会与扩展建议:复现论文的正确打开方式

整个项目做下来,我最大的体会有两点。第一点,复现论文的核心不是让代码跑通,而是让模型逻辑自洽。跑通只代表代码没有语法错误,不代表模型正确。一定要做“边界测试”——把某个约束系数改成极端值,看结果是否按预期方向变化,这是检验模型逻辑最有效的方式。第二点,Python 代码只是载体,真正花时间的永远是数据清洗、参数标定和结果解读。我在运行模型上花的时间可能不到总时间的 20%,剩下 80% 都在“找数据、调参数、画图、和论文对比结果”。

如果你准备在这个课题上继续深挖,我认为有三个方向可以扩展。第一是把确定性调度换成滚动优化,每 15 分钟重新求解一次,加入实时预测数据,更贴近实际调度系统。第二是引入多目标优化,把碳排放和运行成本放在一起做帕累托前沿分析,这也是最近论文的热门走向。第三是考虑交通网和电网的耦合——电动车不只在配网侧充电,还涉及路径选择和充电站排队,那就需要交通流模型和电力调度模型联合求解了。

最后分享一个小技巧。论文里的参数往往只给典型值,不会给敏感性区间。你在复现时可以专门批注一份“参数偏离分析”文档,把每个关键参数说清楚“如果翻倍会怎样,如果减半会怎样”。这份材料在答辩时不用全部展示,但只要被问到一个相关问题,你都能立刻给出定量回答,这个优势是很多只跑通代码的人不具备的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询