简介:本资源面向电气工程、能源系统方向的毕业设计与科研入门者,提供一套虚拟电厂/微网日前随机优化调度的完整源程序,重点解决光伏出力与负荷功率双重不确定性下的调度建模难题。程序采用随机规划思路,先基于蒙特卡洛算法对预测的光伏及负荷曲线进行场景生成,再通过快概率距离快速消除法将场景削减至5个,最后在多场景下优化虚拟电厂调度策略,整体实现效果良好。资源包为rar压缩格式,大小约3.66MB,内含源程序及相关参考文档,涵盖随机优化调度模型、交直流混合配电网多阶段随机调度等延伸阅读材料,便于对照理解建模逻辑与代码结构。目前已有86人浏览学习,适合希望掌握不确定性建模、场景生成与削减、随机调度求解等关键环节的读者,可据此复现实验流程、理解算法细节并迁移至自身课题。
1. 虚拟电厂日前调度遇上源荷双重不确定性,这套随机优化程序到底怎么跑
光伏预测曲线早上看着能顶到 80% 出力,中午一片云过来直接掉到 30%,负荷侧晚高峰又比预测多出 15%——这种场景做微网调度的人都不陌生。如果按确定性模型排计划,调度方案在真实运行里基本就是废纸。这套 141 号资源要解决的就是这个问题:把光伏出力和负荷功率的双重不确定性同时塞进日前调度模型,用蒙特卡洛生成场景、快概率距离削减到 5 个典型场景,再跑随机优化得到一组对多场景都稳健的调度策略。适合正在做虚拟电厂、微网方向毕业设计或课题的同学,也适合需要快速搭一个随机调度验证框架的从业者。程序本身是完整的源程序,不是伪代码片段,拿到手能直接跑通、改参数、换数据。
2. 源荷双重不确定性怎么建模:从蒙特卡洛场景生成到快概率距离削减
2.1 为什么确定性调度在虚拟电厂里会翻车
虚拟电厂的核心矛盾在于:调度计划是提前一天排的,但光伏和负荷在当天实际运行时都会偏离预测值。确定性模型只拿一条预测曲线去优化,等价于假设预测 100% 准确。实际工程里,光伏预测误差在晴天大约 5%~10%,多云天能到 20%~30%;负荷预测误差通常在 3%~8%,极端天气或节假日更大。两个误差叠加,确定性方案要么保守到浪费调节能力,要么激进到实际运行时越限。
随机规划的思路不是去猜一个“更准的预测值”,而是承认预测有误差分布,把误差的统计特性通过大量场景表达出来,再在这些场景上做优化。这样得到的调度策略不依赖某一条曲线,而是对一整簇可能情况都可行。常见做法是:先假设光伏预测误差服从正态分布(均值 0、标准差按预测出力的一定比例取),负荷误差同理,然后用蒙特卡洛抽样生成大量场景。
2.2 蒙特卡洛场景生成:把误差分布变成可计算的场景集
蒙特卡洛的核心操作就三步:确定随机变量的概率分布、抽样、组合成场景。光伏出力和负荷功率各抽 N 次,每次得到一个 24 小时的曲线,两条曲线拼成一个场景。下面是程序里场景生成的核心逻辑,我用 Python 重写了一个等价版本方便理解参数含义:
import numpy as np def generate_scenarios(pv_forecast, load_forecast, n_scenarios=1000, pv_sigma_ratio=0.15, load_sigma_ratio=0.05, seed=42): """ pv_forecast: 光伏预测出力曲线, shape=(24,), 单位 kW load_forecast: 负荷预测曲线, shape=(24,), 单位 kW n_scenarios: 蒙特卡洛抽样次数 pv_sigma_ratio: 光伏预测误差标准差占预测值的比例 load_sigma_ratio: 负荷预测误差标准差占预测值的比例 """ rng = np.random.default_rng(seed) T = len(pv_forecast) scenarios = np.zeros((n_scenarios, 2, T)) # [场景, 0=光伏/1=负荷, 时段] for i in range(n_scenarios): # 光伏误差: 正态分布, 标准差随预测值大小浮动 pv_sigma = pv_forecast * pv_sigma_ratio pv_noise = rng.normal(0, 1, T) * pv_sigma pv_scenario = np.clip(pv_forecast + pv_noise, 0, None) # 出力不能为负 # 负荷误差: 同样正态, 但比例更小 load_sigma = load_forecast * load_sigma_ratio load_noise = rng.normal(0, 1, T) * load_sigma load_scenario = np.clip(load_forecast + load_noise, 0, None) scenarios[i, 0, :] = pv_scenario scenarios[i, 1, :] = load_scenario return scenarios这段代码里几个参数直接决定场景质量。n_scenarios取 1000 是常见起点,太少(比如 100)会导致场景集覆盖不足,削减后剩下的 5 个场景代表性差;太多(比如 10000)计算量上去但精度提升有限,一般 500~2000 够用。pv_sigma_ratio和load_sigma_ratio是最需要根据实际数据调的:如果手头有历史预测误差统计,直接算标准差比例填进去;没有的话,光伏取 0.1~0.2、负荷取 0.03~0.08 是文献里常见的经验范围。np.clip那一步别省,光伏夜间出力为负、负荷为负在物理上没意义,不截断会让后续优化出现莫名其妙的解。
2.3 快概率距离削减:1000 个场景怎么砍到 5 个还不丢关键信息
场景生成完不能直接扔进优化模型——1000 个场景意味着 1000 组约束,求解时间直接爆炸。场景削减的目标是用少量典型场景近似原始场景集的概率分布。快概率距离削减(Fast Forward Selection)的思路是:每次从剩余场景里选一个“距离已选场景集最远”的加进来,直到达到目标数量。
这里的“距离”用的是 Kantorovich 距离,程序里通常用场景对之间的 2-范数近似。具体操作:先算所有场景对之间的距离矩阵,然后迭代选择。下面是对应的削减代码:
def fast_forward_reduction(scenarios, target_n=5): """ scenarios: shape=(n_scenarios, 2, T) target_n: 削减后保留的场景数 返回: 保留的场景索引, 以及每个保留场景的概率权重 """ n = scenarios.shape[0] # 展平每个场景为一个向量, 方便算距离 flat = scenarios.reshape(n, -1) # 计算场景间欧氏距离矩阵 dist = np.zeros((n, n)) for i in range(n): diff = flat - flat[i] dist[i] = np.sqrt((diff ** 2).sum(axis=1)) selected = [] # 第一步: 选距离其他所有场景之和最大的那个 first = np.argmax(dist.sum(axis=1)) selected.append(first) while len(selected) < target_n: # 对每个未选场景, 算它到已选场景集的最小距离 remaining = [i for i in range(n) if i not in selected] min_dist = np.array([dist[i, selected].min() for i in remaining]) # 选最小距离最大的那个加入 next_idx = remaining[np.argmax(min_dist)] selected.append(next_idx) # 概率权重: 每个保留场景代表多少个原始场景 # 简单做法是均匀分配, 更精细的做法按最近邻归属统计 weights = np.ones(target_n) / target_n return selected, weights削减到 5 个场景是这套程序的设定,也是很多文献的默认值。为什么是 5 而不是 3 或 10?3 个场景对双重不确定性的表达太粗,光伏和负荷的联合分布容易被压扁;10 个场景计算量又上去了,而且边际信息增益递减。5 个场景在精度和速度之间是个比较稳的折中。权重分配这里用的是均匀权重,实际程序里可能用更精细的“按最近邻归属”方式——每个原始场景归到离它最近的保留场景,统计归属数量算权重。如果你发现削减后场景概率和原始分布偏差大,优先检查权重计算这块。
注意:场景削减的顺序会影响结果。快概率距离削减是贪心算法,第一步选谁很关键。程序里用“距离和最大”作为第一步选择标准是合理的,但如果你换一组随机种子生成场景,削减结果会变。建议固定随机种子,保证结果可复现。
3. 随机优化调度模型怎么搭:目标函数、约束条件和求解器配置
3.1 目标函数:多场景期望成本最小化
随机优化调度和确定性调度的核心区别在目标函数。确定性模型最小化单场景的运行成本,随机模型最小化所有场景的期望成本:
$$\min \sum_{s=1}^{S} \pi_s \cdot C_s$$
其中 $S=5$ 是削减后的场景数,$\pi_s$ 是场景 $s$ 的概率权重,$C_s$ 是场景 $s$ 下的运行成本。运行成本通常包括:从主网购电成本、燃气轮机/柴油机发电成本、储能充放电损耗成本、弃光惩罚成本。程序里具体包含哪几项,拿到源码后看目标函数定义那一段就能确认。
这里有个容易踩的坑:期望成本最小化不等于每个场景都可行。随机规划保证的是“期望最优”,但某个极端场景下可能仍然出现功率不平衡。如果毕设或课题要求“所有场景下都可行”,需要把模型改成鲁棒优化或者加机会约束。这套程序用的是随机规划,不是鲁棒优化,这个边界要清楚。
3.2 约束条件:功率平衡、设备出力和储能 SOC
约束条件分几类,每类在每个场景下都要满足:
功率平衡约束:每个时段、每个场景下,光伏出力 + 燃气轮机出力 + 储能放电 + 从主网购电 = 负荷 + 储能充电 + 向主网售电。这是最核心的约束,程序里通常写成等式。
设备出力上下限:燃气轮机有最小和最大出力,储能充放电功率有上限,购售电功率有上限。这些是不等式约束。
储能 SOC 约束:储能荷电状态要在 [SOC_min, SOC_max] 之间,且相邻时段 SOC 变化要满足充放电功率和效率的关系。SOC 约束是跨时段的,写的时候注意时间耦合。
爬坡约束:燃气轮机相邻时段出力变化不能超过爬坡率限制。
下面是一个简化的约束构建代码片段,展示怎么用 Python 的优化建模库(比如 PuLP 或 Pyomo)搭这个模型:
import pulp def build_stochastic_dispatch(scenarios, weights, params): """ scenarios: 削减后的场景集, shape=(S, 2, T) weights: 场景概率, shape=(S,) params: 设备参数字典 """ S, _, T = scenarios.shape prob = pulp.LpProblem("VPP_Stochastic_Dispatch", pulp.LpMinimize) # 决策变量: 每个场景、每个时段 pg = pulp.LpVariable.dicts("pg", (range(S), range(T)), lowBound=0) # 燃气轮机 pbuy = pulp.LpVariable.dicts("pbuy", (range(S), range(T)), lowBound=0) # 购电 psell = pulp.LpVariable.dicts("psell", (range(S), range(T)), lowBound=0)# 售电 pch = pulp.LpVariable.dicts("pch", (range(S), range(T)), lowBound=0) # 充电 pdis = pulp.LpVariable.dicts("pdis", (range(S), range(T)), lowBound=0) # 放电 soc = pulp.LpVariable.dicts("soc", (range(S), range(T+1)), lowBound=params["soc_min"], upBound=params["soc_max"]) # 目标: 期望成本 cost = 0 for s in range(S): for t in range(T): cost += weights[s] * ( params["price_buy"][t] * pbuy[s][t] - params["price_sell"][t] * psell[s][t] + params["cost_gas"] * pg[s][t] + params["cost_deg"] * (pch[s][t] + pdis[s][t]) ) prob += cost # 约束 for s in range(S): for t in range(T): pv = scenarios[s, 0, t] load = scenarios[s, 1, t] # 功率平衡 prob += pv + pg[s][t] + pdis[s][t] + pbuy[s][t] == \ load + pch[s][t] + psell[s][t] # 燃气轮机出力上限 prob += pg[s][t] <= params["pg_max"] # 购售电互斥(简化处理: 用上限约束代替) prob += pbuy[s][t] <= params["grid_max"] prob += psell[s][t] <= params["grid_max"] # 储能 SOC 递推 prob += soc[s][t+1] == soc[s][t] + \ (params["eta_ch"] * pch[s][t] - pdis[s][t] / params["eta_dis"]) * params["dt"] # SOC 首末约束 prob += soc[s][0] == params["soc_init"] prob += soc[s][T] >= params["soc_end_min"] return prob这段代码里几个参数需要根据实际设备填:pg_max是燃气轮机最大出力,grid_max是并网点功率上限,eta_ch和eta_dis是储能充放电效率(通常 0.9~0.95),soc_min和soc_max一般取 0.1 和 0.9,soc_init是初始 SOC。price_buy和price_sell是分时电价曲线,这个直接影响调度策略——峰谷价差越大,储能套利空间越大,调度结果里储能充放电越频繁。
3.3 求解器选择和求解时间控制
程序里用的求解器可能是 Gurobi、CPLEX 或者开源的 CBC/GLPK。如果源码里用的是 Gurobi,你本地没 license 的话需要换成 CBC 或 HiGHS。PuLP 默认自带 CBC,对小规模问题够用。5 个场景、24 个时段、6 类决策变量,变量规模大约 5×24×6=720 个连续变量,CBC 求解通常在几秒到几十秒。如果换成 1000 个场景不削减,变量数直接到 14 万以上,CBC 可能跑不动,这也是为什么要做场景削减。
求解时间还受约束数量影响。功率平衡约束是每个场景每个时段一条,5×24=120 条;SOC 递推也是 120 条;加上其他不等式约束,总约束数在几百条量级。如果求解超过 1 分钟还没出结果,先检查是不是场景数没削减到位,或者某些约束写成了非线性形式。
提示:如果要把这套程序扩展到多日调度或者加入更多设备类型,建议先把场景数控制在 5~10,确认模型能跑通再逐步加复杂度。一上来就堆设备堆场景,调试成本会高很多。
4. 避坑与排查:跑这套随机调度程序时最容易翻车的五个地方
4.1 场景削减后概率权重之和不等于 1
现象:优化结果里期望成本算出来偏大或偏小,和手动核算对不上。
原因:削减后的场景权重没有归一化,或者权重分配逻辑有 bug。均匀权重时 5 个场景各 0.2,和正好是 1;但如果用最近邻归属统计,某些场景可能没被分配到任何原始场景,权重为 0。
解决:削减完成后加一行检查assert abs(weights.sum() - 1.0) < 1e-6,不满足就手动归一化。同时检查每个保留场景的权重是否都大于 0,有 0 权重的场景等于白保留。
4.2 光伏场景出现负出力导致优化不可行
现象:求解器报 infeasible,或者解出来的光伏出力是负数。
原因:蒙特卡洛抽样时正态分布的尾部可能抽到负值,如果没做截断,负的光伏出力进入功率平衡约束,物理上无解。
解决:生成场景后对光伏和负荷都做np.clip(x, 0, None)。另外检查一下光伏预测曲线本身在夜间时段是否已经置零,如果预测曲线夜间有微小正值,叠加负噪声后更容易出问题。
4.3 储能 SOC 递推公式量纲不一致
现象:SOC 曲线在相邻时段跳变异常,或者 SOC 很快撞到上下限。
原因:SOC 递推里功率乘以时间步长dt的单位没统一。如果功率是 kW,dt是小时,那 SOC 变化量的单位是 kWh,但 SOC 本身如果是百分比或标幺值,还需要除以储能容量。
解决:统一用 kWh 表示 SOC 绝对量,递推公式写成soc[t+1] = soc[t] + (eta_ch * pch[t] - pdis[t] / eta_dis) * dt,其中 soc 单位 kWh,pch/pdis 单位 kW,dt 单位小时。如果 soc 用百分比,递推后要除以容量再乘 100。
4.4 购售电同时非零导致成本计算错误
现象:优化结果里同一时段既购电又售电,成本被重复计算或抵消。
原因:模型里购电和售电是两个独立变量,如果没有互斥约束,求解器可能让两者同时非零来“套利”——以低价购电同时以高价售电,这在物理上不成立。
解决:加二进制变量做互斥约束,或者用购售电价差保证不会同时非零(购电价始终高于售电价时,同时非零会被优化自然排除)。如果程序里没加互斥,检查一下购售电价设置是否满足price_buy > price_sell。
4.5 换一组数据后求解时间暴涨
现象:原来几秒出结果,换了光伏和负荷数据后跑了几分钟还没完。
原因:新数据的波动性更大,场景削减后保留的场景之间差异大,优化问题的可行域形状变复杂,分支定界树变大。另外如果新数据里出现了极端值(比如负荷突然翻倍),可能导致某些约束边界收紧,求解器需要更多迭代。
解决:先检查新数据的量纲和范围是否和原数据一致,单位从 kW 变成 MW 这种错误会让约束完全变形。确认数据没问题后,可以适当放宽求解器的 gap 容忍度(比如从 1e-6 放到 1e-3),或者增加场景削减的目标数到 7~8 让场景集更平滑。
5. 从跑通到用对:场景数敏感性测试与结果验证的实操技巧
程序跑通只是第一步,真正要拿这套东西做毕设或者写论文,得知道结果可不可信。我一般会做两件事:场景数敏感性测试和样本外验证。
场景数敏感性测试的操作很直接:把削减目标从 3 改到 10,每个值跑一遍,记录最优期望成本和求解时间。如果从 5 到 7 成本变化小于 1%,说明 5 个场景已经够用;如果从 5 到 7 成本跳了 5% 以上,说明场景削减不够,需要增加保留场景数。下面是一段批量测试的代码框架:
results = [] for n_keep in [3, 5, 7, 10]: selected, weights = fast_forward_reduction(scenarios, target_n=n_keep) reduced = scenarios[selected] prob = build_stochastic_dispatch(reduced, weights, params) prob.solve(pulp.PULP_CBC_CMD(msg=0)) results.append({ "n_scenarios": n_keep, "expected_cost": pulp.value(prob.objective), "solve_time": prob.solutionTime }) for r in results: print(f"场景数={r['n_scenarios']}, 期望成本={r['expected_cost']:.2f}, " f"求解时间={r['solve_time']:.2f}s")样本外验证更关键:用蒙特卡洛另外生成一批测试场景(不和训练场景共用随机种子),把优化得到的调度策略(各设备各时段的出力计划)代入测试场景,检查每个场景下功率是否平衡、设备是否越限。如果测试场景下有 5% 以上不可行,说明场景削减保留的信息不够,或者模型约束太紧。这个验证步骤在论文里写出来,比只报一个期望成本有说服力得多。
还有一个容易被忽略的点:削减后的 5 个场景,它们的光伏和负荷曲线长什么样,最好画出来看一眼。如果 5 条曲线几乎重合,说明削减没起到代表极端情况的作用;如果有一条曲线明显偏离,检查它是不是被赋予了过高的权重。场景可视化是判断削减质量最直观的手段,比看数字快得多。
从那以后我每次跑随机调度程序,都强制走一遍“削减后场景可视化 → 场景数敏感性 → 样本外可行性验证”这三步,少一步都不敢把结果往论文里放。希望帮到你。
本文还有配套的精品资源,点击获取