
简介面向微电网与电力系统优化领域的研究人员和工程师这套微网综合能源源代码尤其适合有一定优化基础、希望将鲁棒优化方法落地到微网容量配置场景的硕博研究生与工程技术人员。它基于两阶段鲁棒优化算法解决微网多电源容量配置问题重点应对风光出力波动、负荷需求变化等不确定性条件下的电源容量决策与运行经济性问题。算法分两阶段第一阶段在已知信息下确定各能源单元初始容量第二阶段依据实际发生的不确定性场景进行再调整兼顾规划方案的最优性与抗风险能力。包内共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 第一阶段与第二阶段怎么咬合CCG 的迭代逻辑两阶段鲁棒模型写出来是 min-max-min 三层结构不能直接丢给求解器。业内最常规的解法是列与约束生成CCG算法通过主问题与子问题交替迭代逼近最优解。主问题承担第一阶段投资决策同时维护一条条来自子问题的割约束。子问题则固定第一阶段的装机容量在内层求运行成本最小、外层求不确定量最恶劣从而找到让当前方案最难受的风光出力场景并把对应的运行约束以割的形式反馈给主问题。子问题每找到一条新割主问题就会重新优化一次投资组合迭代若干轮后上下界收敛容量配置方案也就稳定了。CCG 和 Benders 分解最明显的差别在于Benders 往主问题里加的是对偶割只有成本信息CCG 往主问题里加的是完整的运行变量和约束相当于把“最恶劣场景”的调度子问题直接嵌入主问题。CCG 在组合优化问题上收敛轮数通常少很多微网容量配置这种问题一般 510 轮就能收敛到 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 主问题骨架投资变量与 CCG 割约束主问题负责投资决策和接收从子问题返回的割。首次迭代时还没有割约束目标函数里只有投资成本和运行成本变量 θ 的初始值之后每一轮迭代都会向模型里添加一条割把 θ 的下界逐步抬高。# 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, domainpyo.NonNegativeReals) # 运行成本代理变量由子问题的割约束限定下界 m.theta pyo.Var(domainpyo.Reals) # 容量上限约束防止优化结果出现不合理的超大装机 def cap_limit_rule(m, g): return m.cap[g] cap_upper[g] m.cap_limit pyo.Constraint(m.gen, rulecap_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(ruleobj_rule, sensepyo.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(initializeT) # 不确定变量各时段光伏和风电的实际出力 sp.pv pyo.Var(sp.T, withinpyo.Reals) sp.wt pyo.Var(sp.T, withinpyo.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, rulepv_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, rulewt_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(rulebudget_rule)这里abs表达式在 Pyomo 中是可以直接使用的会自动线性化。子问题的目标函数要依赖内层运行调度的对偶形式才能写成 max篇幅有限无法完整展开但核心思想是内层 min 是一个线性规划写出其对偶后目标函数变成对偶变量与不确定变量的乘积。这个双线性项会让问题变成非凸常规做法是用大 M 法引入辅助整数变量或者更简单一点直接枚举盒式不确定集的顶点场景来避开双线性项。对 24 时段问题顶点数量不算离谱但 8760 时段就不要枚举了。在实际复现中我建议第一版先做“枚举顶点 外层 max 选最差”跑通后再优化成对偶线性化版本。这样能保证你先得到一个能收敛的基准再去升级算法细节。3.4 CCG 主循环上下界更新与割迭代外层迭代是整个算法的主干。每次迭代先解主问题得到容量配置和 θ 值把目标函数值记为下界再固定容量配置解子问题子问题的目标函数值加上投资成本就是上界两者间隙小于容差时停止。# 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(fIter {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)综合成本 (万元)032001200150036804310016001800395083000210020004180122950250022004360242900300025004620看出规律没有光伏装机随增大略微下降因为光伏出力波动对鲁棒性威胁最大储能和柴油机容量则明显上升用来兜底。综合成本从 3680 万涨到 4620 万涨幅约 25%。如果只跑一个你会得到一个“看起来还行”的方案但完全不清楚它在真实天气下的稳健性。经验上取时段数的三分之一比较均衡但这不是铁律。如果你的园区有燃气轮机可以快速响应可以取小一点如果只有柴油机这种慢速机组建议取大一些。做报告时把这条敏感性曲线附上比单纯给一个配置结果要有说服力得多。4.2 收敛容差与迭代上限的经验搭配CCG 外层循环的容差我一般设 1%迭代上限 20 轮。容差设太小有两个代价一是求解时间成倍增加二是子问题求解器的数值噪声会干扰外层收敛判断出现“gap 在 0.5% 附近来回震荡”的假象。子问题内部的求解间隙也要单独设置。Gurobi 的默认 MIP gap 对子问题来说往往过松导致每一轮反馈给主问题的割都不够紧。常见做法是让子问题的 MIP gap 比外层小一个量级比如外层 1%子问题 0.1%。如果子问题里没有整数变量纯线性规划求解精度本来就很高不太需要额外设置。迭代轮数超过 10 轮仍未收敛时不要盲目增大迭代上限而是检查割约束是否重复。同一个最劣场景被重复加入主问题两次以上说明子问题求解不稳定或者不确定集建模有误。此时可以通过打印每一轮的出力场景来定位问题。4.3 惩罚系数切负荷惩罚该定多大微网容量配置里几乎都有切负荷变量用于保证模型在极端场景下仍有可行解。但切负荷惩罚系数的大小会直接影响配置结果设小了模型宁可切负荷也不装储能设大了又会过度投资。我一般取单位缺电损失的 1.52 倍作为惩罚系数下限。另外一个不那么直观的参数是储能循环寿命。很多代码把储能当成一个“大号电池”来建模忽略循环次数对寿命的损耗。配置结果往往会偏大因为模型让储能每天都深度充放。更稳的做法是在子问题里加一个日充放电量的上限约束或者把循环损耗折算进运行成本。这两种处理方式对储能的配置结果差异可以达到 15% 以上。5. 容量配置避坑指南五个最常见的翻车现场5.1 现象迭代不收敛子问题目标值持续跳变CCG 跑到第八轮上下界还在来回震荡甚至子问题目标值忽高忽低。最常见原因是子问题求解精度不足特别是当你用默认 MIP gap 求解含整数变量的子问题时每一轮返回的最劣场景都不稳定割约束自然也不稳定。解决办法给子问题单独设置更严格的求解容差并把每个轮次的最劣场景打印出来对比。如果场景变化幅度很大说明问题不在收敛容差而在不确定集约束写错比如预算约束里的归一化分母写成预测值而不是偏差值。5.2 现象两阶段鲁棒结果比单阶段还乐观鲁棒优化的结果应该比单阶段保守至少成本不应该明显更低。出现这种情况优先怀疑子问题目标函数的符号写反了。内层 min 是运行成本最小化外层 max 是找最恶劣场景两者如果颠倒子问题就会去找“对系统最有利”的场景每一轮割约束都给得很松最终引导主问题装一个激进的低容量方案。解决方法是加一道校验把设为 0 跑一遍结果必须与单阶段确定性模型基本一致。如果不一致说明嵌套结构写错先修正结构再谈鲁棒性。5.3 现象小算例跑得动规模一放大就内存爆炸24 时段没问题换成 8760 时段后内存直接撑爆。原因是 CCG 每轮都会把子问题完整地“拷贝”进主问题割约束和变量数量随迭代轮数线性增长加上时段维度的放大模型规模翻了几十倍。我一般会做三件事一是把 8760 小时聚类成若干个典型日按权重建模而不是直接全时段跑二是主问题里只保留当前轮次所需的割约束历史割如果已非活跃可以主动剔掉三是给 Pyomo 设置更积极的求解器参数比如开启 Gurobi 的节点文件存储避免内存溢出。多数情况下聚类处理后精度损失很小内存压力降一个量级。5.4 现象配置结果对负荷曲线异常敏感换一条负荷曲线容量配置结果完全变样。原因往往是你只用了单一典型日数据。微网负荷有很强的季节性和工作日/休息日差异单条曲线只能代表一种工况。两阶段鲁棒模型处理的是电源出力不确定性对负荷本身也会敏感但不能把负荷也当成已知量。解决方法是构造四季典型日或者至少区分旺季和淡季再按比例加权进入模型。常见做法是把一年负荷聚类成 46 条典型曲线每条曲线单独建立运行约束投资阶段共享容量变量。这样配置结果对负荷的敏感度会明显下降。5.5 现象代码改了输入数据后结果不变这个问题看起来很低级但实际经常发生。原因不外乎三种代码里数据文件名写死且没有检查文件更新外部参数在代码里硬编码覆盖了输入文件或者缓存机制没有失效。我见过最隐蔽的情况是CSV 里新增了一列读取代码只取前两列新数据根本没被读进去。解决办法是把所有输入路径集中到一个配置文件中每次运行前打印关键参数的哈希值比如对负荷序列计算一个简单的校验和运行日志里输出。这样任何数据更新都能在日志里看到痕迹排查起来快得多。6. 从跑通到敢用收敛性验证与结果解读的几个技巧模型收敛不代表方案可以落地这一步常常被忽略。拿到容量配置结果后我一般会做三件事。第一件事是画收敛曲线把主问题下界和上界随迭代轮次的变化画出来确认 gap 是单调下降而非靠运气收敛。如果曲线呈锯齿状即使最终 gap 很小也要重新审查子问题稳定性因为实际业务中很难接受一个“碰巧收敛”的结果。第二件事是回放验证把得到的光伏、储能和柴油机容量固定下来用历史一年的实际风光出力数据重新做运行模拟统计切负荷小时数和柴油机启停次数。模拟结果与模型理想结果差距小于 5%说明模型建模合理差距过大就要回头检查不确定集偏差设置是否符合当地气候规律。第三件事是敏感性分析不只在模型参数层面做而是在方案层面做。比如储能容量增加 10%综合成本上升多少、切负荷风险下降多少柴油机容量减少 20%系统抗扰动能力削弱多少。这类敏感性数据能直接支撑项目评审时的技术问答也能让你判断出当前方案是“成本驱动型”还是“可靠性驱动型”。如果两者的边际收益差距很小说明当前配置已经接近帕累托前沿再迭代优化的收益有限可以收手了。我自己早期做容量配置时偷懒只取一条典型日曲线就跑完了整个流程报了一个很便宜的投资方案。后来做并网评审时被问到“连续三天阴雨怎么办”当场答不上来只能回去重做。从那以后任何鲁棒优化结果都必须过三道关先做敏感性扫描再做全年数据回放最后做极端天气场景抽查。这三步走完方案才敢拿出手。希望这套方法和坑位总结能帮你在微网容量配置这道题上少走几步弯路。本文还有配套的精品资源点击获取