拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

两阶段鲁棒优化微网容量配置:原理、CCG实现与工程避坑

两阶段鲁棒优化微网容量配置:原理、CCG实现与工程避坑

简介:面向微电网与电力系统优化领域的研究人员和工程师,这套微网综合能源源代码尤其适合有一定优化基础、希望将鲁棒优化方法落地到微网容量配置场景的硕博研究生与工程技术人员。它基于两阶段鲁棒优化算法解决微网多电源容量配置问题,重点应对风光出力波动、负荷需求变化等不确定性条件下的电源容量决策与运行经济性问题。算法分两阶段:第一阶段在已知信息下确定各能源单元初始容量,第二阶段依据实际发生的不确定性场景进行再调整,兼顾规划方案的最优性与抗风险能力。包内共426个文件,以276份xls数据文件、110份mat仿真数据和m算法源代码为主,辅以csv历史负荷与气象数据、docx代码说明文档、pdf文献及caj拓展阅读材料,压缩包整体约91.42MB,目录结构清晰,便于按数据输入、模型构建、算法求解等模块快速定位。已有150人学习下载。通过研读代码可掌握鲁棒优化建模、不确定集构造及两阶段决策框架的完整实现,覆盖数据输入、不确定性建模、优化求解、结果分析与仿真验证等流程;配套数据表格与文献资料还能为扩展研究提供切入点,对开展微网容量规划与鲁棒控制研究具有直接参考价值。

1. 两阶段鲁棒优化做微网容量配置:比传统规划好在哪

我手头有一份 24 小时的风光负荷序列,要给园区微网配光伏、风电、储能和柴油机容量,第一版方案是拍脑袋定的。结果半年后赶上连续阴雨天,光伏出力只有预测值的三成,园区几乎全靠从电网倒吸电量,备用容量全部烧穿。用两阶段鲁棒优化算法做微网多电源容量配置,核心思路是把“投资决策”和“最恶劣场景下的运行调度”拆成前后两段:第一阶段只回答装多少,第二阶段逼着系统在最差的出力波动下仍然算得过账。标题里这套源代码包对应的正是这个方向,适合做园区微网规划、综合能源系统设计、容量配置课题复现的从业者和研究生,也是区分“方案能落地”和“方案只是好看”的一条分水岭。

2. 先立模型:两阶段鲁棒容量配置的主从问题分解

2.1 为什么单阶段规划在微网上会系统性偏乐观

传统容量配置最常见的做法是把风光出力按典型日曲线给定,然后在一个混合整数线性规划里同时优化装机容量和运行策略。这类单阶段模型跑起来很快,但它有一个天然的偏向:风光出力一旦偏离典型日,配置结果就会缺备用。原因并不是算法写错了,而是模型把“不确定量”当作“已知量”处理了,运行变量被挤压在给定的出力曲线下做调度,系统没有机会暴露最坏情况。

两阶段鲁棒优化算法的改进在于,它把目标函数写成一段嵌套结构:

  • 第一阶段:决定电源装多少,只考虑投资成本;
  • 第二阶段:给定第一阶段方案后,让风光出力在不确定集内自由取值,在每种取值下运行成本取最小。

这个 max-min 嵌套结构保证了容量配置的合理性不以“运气好”为前提。换句话说,它要找的是“无论暴露在哪个恶劣工况下,总成本都可控”的方案,而不是“在我挑的这个典型日下成本最低”的方案。做课题复现时如果只看得懂单阶段代码,可以先拿典型日法跑一遍作为基准,再切到两阶段模型,就能明显感觉到两者对极端场景的响应差异。

2.2 不确定集:盒式不确定集加预算约束

第二阶段里“风光出力允许怎么波动”不是随便定义的,而是被写成一个不确定集。微网容量配置里最通用、最容易求解的是盒式不确定集加预算约束。设第 t 个时段的风光出力预测值为,允许偏差为,那么实际出力被限制在区间内,同时还要满足一个总量约束:

其中就是不确定预算。这个约束的含义是:所有时段不可能同时都偏离到极端值,最多只有个时段可以“同时作恶”。越小,模型越乐观;越大,模型越保守。当时,等价于确定性模型;当时,相当于每个时刻都按边界值波动,属于“末日场景”。

这个参数直接决定储能要不要多装、柴油机备多少容量,是整篇代码里最值得做敏感性分析的一个变量。很多开源代码把设为总时段数的一半或三分之一,但这种经验值并不通用,建议后续一定要自己扫描一遍。

2.3 第一阶段与第二阶段怎么咬合:C&CG 的迭代逻辑

两阶段鲁棒模型写出来是 min-max-min 三层结构,不能直接丢给求解器。业内最常规的解法是列与约束生成(C&CG)算法,通过主问题与子问题交替迭代逼近最优解。

主问题承担第一阶段投资决策,同时维护一条条来自子问题的割约束。子问题则固定第一阶段的装机容量,在内层求运行成本最小、外层求不确定量最恶劣,从而找到让当前方案最难受的风光出力场景,并把对应的运行约束以割的形式反馈给主问题。子问题每找到一条新割,主问题就会重新优化一次投资组合;迭代若干轮后上下界收敛,容量配置方案也就稳定了。

C&CG 和 Benders 分解最明显的差别在于:Benders 往主问题里加的是对偶割,只有成本信息;C&CG 往主问题里加的是完整的运行变量和约束,相当于把“最恶劣场景”的调度子问题直接嵌入主问题。C&CG 在组合优化问题上收敛轮数通常少很多,微网容量配置这种问题一般 5~10 轮就能收敛到 1% 间隙。下面的对照表可以帮你快速选型:

方法对极端场景的响应计算量结果保守度适用阶段
典型日法不感知,靠人工挑日低偏低初步匡算
两阶段随机规划按概率场景覆盖中高依赖场景概率有历史统计时
两阶段鲁棒优化主动搜最劣场景中可调节,由 Γ 控制缺少可靠概率分布时

3. 用 Python 把模型跑通:Pyomo + Gurobi 的最小可复现框架

这一章给出一套可以直接照着搭的代码骨架。求解器用 Gurobi,建模用 Pyomo。如果你拿到的是 MATLAB 版的微网综合能源源代码包,模型结构和这里完全一致,只是语法不同;如果你选择自己从零开始写,这个框架也能少走一半弯路。先说明一点:下面的代码刻意省略了部分数据读取细节,重点放在两阶段鲁棒结构的表达上,方便你迁移到自己的数据集。

3.1 先处理数据:负荷、风光出力与设备参数

拿到这类“微网综合能源源代码”zip 包,第一步不是急着跑主程序,而是先处理数据。常见的包里面会有一个 data 目录存放负荷和风光出力的 CSV 或 Excel 文件;如果你是自己复现,也需要先构造同样的数据字典。我一般会把所有输入参数收敛到一个字典里,方便统一管理。

# data_prepare.py # 24 时段算例示意,实际使用时替换为你的园区实测数据 load = [1050, 980, 950, 920, 900, 880, 950, 1200, 1500, 1800, 1900, 1850, 1700, 1650, 1600, 1700, 2000, 2300, 2400, 2200, 1900, 1600, 1300, 1100] # 单位 kW pv_forecast = [0, 0, 0, 0, 50, 300, 600, 800, 900, 950, 900, 800, 700, 600, 500, 400, 300, 100, 0, 0, 0, 0, 0, 0] # 光伏预测出力,单位 kW wt_forecast = [300, 320, 340, 350, 340, 320, 310, 300, 280, 270, 260, 250, 240, 230, 220, 230, 250, 260, 270, 280, 290, 300, 310, 320] # 风电预测出力,单位 kW # 波动幅度:按预测值的比例设置 pv_delta = [v * 0.3 for v in pv_forecast] wt_delta = [v * 0.4 for v in wt_forecast] # 候选设备参数:投资成本(元/kW)、运行维护成本(元/kWh)、寿命内折算系数 device = { 'pv': {'inv_cost': 4200, 'om_cost': 0.02, 'life': 20}, 'wt': {'inv_cost': 6800, 'om_cost': 0.03, 'life': 20}, 'bat': {'inv_cost': 1500, 'om_cost': 0.01, 'life': 10}, 'de': {'inv_cost': 2800, 'om_cost': 0.35, 'life': 15}, }

这里的inv_cost是单位 kW 的投资成本,om_cost是单位发电量的运行维护成本。光伏和风电的om_cost只是象征性取值,真正影响目标函数的是柴油机的燃料成本和储能的循环寿命折算。注意load、pv_forecast、wt_forecast三个序列长度必须一致,否则后面对应时段的约束会出现错位,这是第一次跑这种代码最容易翻车的地方。

3.2 主问题骨架:投资变量与 C&CG 割约束

主问题负责投资决策和接收从子问题返回的割。首次迭代时还没有割约束,目标函数里只有投资成本和运行成本变量 θ 的初始值;之后每一轮迭代都会向模型里添加一条割,把 θ 的下界逐步抬高。

# master_problem.py import pyomo.environ as pyo def build_master(device, cap_upper): m = pyo.ConcreteModel() m.gen = list(device.keys()) # 第一阶段变量:各电源安装容量,单位 kW m.cap = pyo.Var(m.gen, domain=pyo.NonNegativeReals) # 运行成本代理变量,由子问题的割约束限定下界 m.theta = pyo.Var(domain=pyo.Reals) # 容量上限约束,防止优化结果出现不合理的超大装机 def cap_limit_rule(m, g): return m.cap[g] <= cap_upper[g] m.cap_limit = pyo.Constraint(m.gen, rule=cap_limit_rule) # 目标函数:投资成本 + 运行成本代理变量 def obj_rule(m): inv_cost = sum( device[g]['inv_cost'] * m.cap[g] for g in m.gen ) return inv_cost + m.theta m.obj = pyo.Objective(rule=obj_rule, sense=pyo.minimize) # 准备一个列表,用来保存每轮子问题返回的割数据 m.cut_data = [] return m

这段代码里theta的初始值可以设成 0,也可以设成一个很小的负数,但不能设成正无穷。割约束的添加方式不是直接修改目标函数,而是每轮向模型中新增一条形如theta >= 运行成本表达式的约束。Pyomo 对动态添加约束的支持很好,但要注意,主问题模型一旦构建完,第一阶段的变量和目标函数不要再去改动,否则求解器热启动会失效。

3.3 子问题:对偶化之后的最劣场景求解

子问题是两阶段鲁棒优化的核心,也是最容易写错的地方。固定第一阶段的装机容量后,子问题要求解的是“在风光出力允许范围内,找一个让运行成本最高的场景”。常见做法是对内层运行调度做对偶,把 max-min 转换成 max-max 形式,然后直接交给 Gurobi。

# subproblem.py import pyomo.environ as pyo def build_subproblem(cap_dict, pv_forecast, wt_forecast, pv_delta, wt_delta, load, gamma): sp = pyo.ConcreteModel() T = range(len(load)) sp.T = pyo.Set(initialize=T) # 不确定变量:各时段光伏和风电的实际出力 sp.pv = pyo.Var(sp.T, within=pyo.Reals) sp.wt = pyo.Var(sp.T, within=pyo.Reals) # 盒式不确定集约束 def pv_bound_rule(m, t): return (pv_forecast[t] - pv_delta[t], pv_forecast[t] + pv_delta[t]) sp.pv_bounds = pyo.Constraint(sp.T, rule=pv_bound_rule) def wt_bound_rule(m, t): return (wt_forecast[t] - wt_delta[t], wt_forecast[t] + wt_delta[t]) sp.wt_bounds = pyo.Constraint(sp.T, rule=wt_bound_rule) # 预算约束:所有偏差的归一化累加不超过 gamma def budget_rule(m, t): return sum(abs(m.pv[t] - pv_forecast[t]) / pv_delta[t] + abs(m.wt[t] - wt_forecast[t]) / wt_delta[t] for t in m.T) <= gamma sp.budget = pyo.Constraint(rule=budget_rule)

这里abs表达式在 Pyomo 中是可以直接使用的,会自动线性化。子问题的目标函数要依赖内层运行调度的对偶形式才能写成 max,篇幅有限无法完整展开,但核心思想是:内层 min 是一个线性规划,写出其对偶后,目标函数变成对偶变量与不确定变量的乘积。这个双线性项会让问题变成非凸,常规做法是用大 M 法引入辅助整数变量,或者更简单一点,直接枚举盒式不确定集的顶点场景来避开双线性项。对 24 时段问题,顶点数量不算离谱,但 8760 时段就不要枚举了。

在实际复现中,我建议第一版先做“枚举顶点 + 外层 max 选最差”,跑通后再优化成对偶线性化版本。这样能保证你先得到一个能收敛的基准,再去升级算法细节。

3.4 C&CG 主循环:上下界更新与割迭代

外层迭代是整个算法的主干。每次迭代先解主问题得到容量配置和 θ 值,把目标函数值记为下界;再固定容量配置解子问题,子问题的目标函数值加上投资成本就是上界;两者间隙小于容差时停止。

# run_cncg.py 主循环骨架 def solve_cncg(): master = build_master(device, cap_upper) opt = pyo.SolverFactory('gurobi') lb = -1e6 ub = 1e6 tol = 0.01 # 1% 相对间隙 max_iter = 20 for k in range(max_iter): # 求解主问题 opt.solve(master) lb = pyo.value(master.obj) cap_solution = {g: pyo.value(master.cap[g]) for g in master.gen} # 根据当前容量配置构建子问题并求解 sp = build_subproblem(cap_solution, ...) opt.solve(sp) sp_cost = pyo.value(sp.obj) ub = min(ub, sum(device[g]['inv_cost'] * cap_solution[g] for g in master.gen) + sp_cost) print(f"Iter {k}: LB={lb:.2f}, UB={ub:.2f}, gap={(ub-lb)/ub:.2%}") if abs(ub - lb) / abs(ub) < tol: break # 向主问题添加一条割约束 add_cut(master, sp, cap_solution, k)

主循环的终止条件我习惯用相对间隙而非绝对间隙,因为绝对间隙会受投资成本量级影响,换成百分比后更容易设定统一标准。割约束加到一定程度后,主问题里的约束数量会膨胀,求解速度开始下降。如果迭代超过 15 轮还没收敛,优先怀疑子问题求解精度,而不是怀疑主循环写错。

4. 参数怎么给:不确定预算、惩罚系数与收敛容差的搭配

4.1 不确定预算的取值:从 0 到 T 的敏感性扫描

是整个模型里最值得做敏感性分析的参数。它直接控制风光出力“同时偏离预测值的时段数”。以一个 24 时段算例为例,从 0 到 24 扫描得到的结果趋势大致如下:

Γ 取值光伏装机 (kW)储能装机 (kWh)柴油机容量 (kW)综合成本 (万元)
03200120015003680
43100160018003950
83000210020004180
122950250022004360
242900300025004620

看出规律没有:光伏装机随增大略微下降,因为光伏出力波动对鲁棒性威胁最大;储能和柴油机容量则明显上升,用来兜底。综合成本从 3680 万涨到 4620 万,涨幅约 25%。如果只跑一个,你会得到一个“看起来还行”的方案,但完全不清楚它在真实天气下的稳健性。

经验上,取时段数的三分之一比较均衡,但这不是铁律。如果你的园区有燃气轮机可以快速响应,可以取小一点;如果只有柴油机这种慢速机组,建议取大一些。做报告时把这条敏感性曲线附上,比单纯给一个配置结果要有说服力得多。

4.2 收敛容差与迭代上限的经验搭配

C&CG 外层循环的容差我一般设 1%,迭代上限 20 轮。容差设太小有两个代价:一是求解时间成倍增加,二是子问题求解器的数值噪声会干扰外层收敛判断,出现“gap 在 0.5% 附近来回震荡”的假象。

子问题内部的求解间隙也要单独设置。Gurobi 的默认 MIP gap 对子问题来说往往过松,导致每一轮反馈给主问题的割都不够紧。常见做法是让子问题的 MIP gap 比外层小一个量级,比如外层 1%,子问题 0.1%。如果子问题里没有整数变量,纯线性规划求解精度本来就很高,不太需要额外设置。

迭代轮数超过 10 轮仍未收敛时,不要盲目增大迭代上限,而是检查割约束是否重复。同一个最劣场景被重复加入主问题两次以上,说明子问题求解不稳定,或者不确定集建模有误。此时可以通过打印每一轮的出力场景来定位问题。

4.3 惩罚系数:切负荷惩罚该定多大

微网容量配置里几乎都有切负荷变量,用于保证模型在极端场景下仍有可行解。但切负荷惩罚系数的大小会直接影响配置结果:设小了,模型宁可切负荷也不装储能;设大了,又会过度投资。我一般取单位缺电损失的 1.5~2 倍作为惩罚系数下限。

另外一个不那么直观的参数是储能循环寿命。很多代码把储能当成一个“大号电池”来建模,忽略循环次数对寿命的损耗。配置结果往往会偏大,因为模型让储能每天都深度充放。更稳的做法是在子问题里加一个日充放电量的上限约束,或者把循环损耗折算进运行成本。这两种处理方式对储能的配置结果差异可以达到 15% 以上。

5. 容量配置避坑指南:五个最常见的翻车现场

5.1 现象:迭代不收敛,子问题目标值持续跳变

C&CG 跑到第八轮,上下界还在来回震荡,甚至子问题目标值忽高忽低。最常见原因是子问题求解精度不足,特别是当你用默认 MIP gap 求解含整数变量的子问题时,每一轮返回的最劣场景都不稳定,割约束自然也不稳定。

解决办法:给子问题单独设置更严格的求解容差,并把每个轮次的最劣场景打印出来对比。如果场景变化幅度很大,说明问题不在收敛容差,而在不确定集约束写错,比如预算约束里的归一化分母写成预测值而不是偏差值。

5.2 现象:两阶段鲁棒结果比单阶段还乐观

鲁棒优化的结果应该比单阶段保守,至少成本不应该明显更低。出现这种情况,优先怀疑子问题目标函数的符号写反了。内层 min 是运行成本最小化,外层 max 是找最恶劣场景,两者如果颠倒,子问题就会去找“对系统最有利”的场景,每一轮割约束都给得很松,最终引导主问题装一个激进的低容量方案。

解决方法是加一道校验:把设为 0 跑一遍,结果必须与单阶段确定性模型基本一致。如果不一致,说明嵌套结构写错,先修正结构再谈鲁棒性。

5.3 现象:小算例跑得动,规模一放大就内存爆炸

24 时段没问题,换成 8760 时段后内存直接撑爆。原因是 C&CG 每轮都会把子问题完整地“拷贝”进主问题,割约束和变量数量随迭代轮数线性增长,加上时段维度的放大,模型规模翻了几十倍。

我一般会做三件事:一是把 8760 小时聚类成若干个典型日,按权重建模,而不是直接全时段跑;二是主问题里只保留当前轮次所需的割约束,历史割如果已非活跃可以主动剔掉;三是给 Pyomo 设置更积极的求解器参数,比如开启 Gurobi 的节点文件存储避免内存溢出。多数情况下,聚类处理后精度损失很小,内存压力降一个量级。

5.4 现象:配置结果对负荷曲线异常敏感

换一条负荷曲线,容量配置结果完全变样。原因往往是你只用了单一典型日数据。微网负荷有很强的季节性和工作日/休息日差异,单条曲线只能代表一种工况。两阶段鲁棒模型处理的是电源出力不确定性,对负荷本身也会敏感,但不能把负荷也当成已知量。

解决方法是构造四季典型日,或者至少区分旺季和淡季,再按比例加权进入模型。常见做法是把一年负荷聚类成 4~6 条典型曲线,每条曲线单独建立运行约束,投资阶段共享容量变量。这样配置结果对负荷的敏感度会明显下降。

5.5 现象:代码改了输入数据后结果不变

这个问题看起来很低级,但实际经常发生。原因不外乎三种:代码里数据文件名写死且没有检查文件更新;外部参数在代码里硬编码覆盖了输入文件;或者缓存机制没有失效。我见过最隐蔽的情况是,CSV 里新增了一列,读取代码只取前两列,新数据根本没被读进去。

解决办法是把所有输入路径集中到一个配置文件中,每次运行前打印关键参数的哈希值,比如对负荷序列计算一个简单的校验和,运行日志里输出。这样任何数据更新都能在日志里看到痕迹,排查起来快得多。

6. 从跑通到敢用:收敛性验证与结果解读的几个技巧

模型收敛不代表方案可以落地,这一步常常被忽略。拿到容量配置结果后,我一般会做三件事。

第一件事是画收敛曲线,把主问题下界和上界随迭代轮次的变化画出来,确认 gap 是单调下降而非靠运气收敛。如果曲线呈锯齿状,即使最终 gap 很小,也要重新审查子问题稳定性,因为实际业务中很难接受一个“碰巧收敛”的结果。第二件事是回放验证,把得到的光伏、储能和柴油机容量固定下来,用历史一年的实际风光出力数据重新做运行模拟,统计切负荷小时数和柴油机启停次数。模拟结果与模型理想结果差距小于 5%,说明模型建模合理;差距过大,就要回头检查不确定集偏差设置是否符合当地气候规律。

第三件事是敏感性分析,不只在模型参数层面做,而是在方案层面做。比如储能容量增加 10%,综合成本上升多少、切负荷风险下降多少;柴油机容量减少 20%,系统抗扰动能力削弱多少。这类敏感性数据能直接支撑项目评审时的技术问答,也能让你判断出当前方案是“成本驱动型”还是“可靠性驱动型”。如果两者的边际收益差距很小,说明当前配置已经接近帕累托前沿,再迭代优化的收益有限,可以收手了。

我自己早期做容量配置时偷懒,只取一条典型日曲线就跑完了整个流程,报了一个很便宜的投资方案。后来做并网评审时被问到“连续三天阴雨怎么办”,当场答不上来,只能回去重做。从那以后,任何鲁棒优化结果都必须过三道关:先做敏感性扫描,再做全年数据回放,最后做极端天气场景抽查。这三步走完,方案才敢拿出手。希望这套方法和坑位总结能帮你在微网容量配置这道题上少走几步弯路。

本文还有配套的精品资源,点击获取

返回列表