ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

两阶段鲁棒优化与CCG算法:微电网不确定性调度的核心方法

两阶段鲁棒优化与CCG算法:微电网不确定性调度的核心方法 1. 两阶段鲁棒优化到底在解决什么问题接触两阶段鲁棒优化这个方向之前我一直在用传统的确定性优化跑微网调度模型也就是把光伏出力、负荷曲线都当成已知量输入一个固定的预测值求解器算出机组出力计划。这个东西做论文仿真确实很顺手但放到实际场景里就发现了一个尴尬的问题预测值只是众多可能场景中的一种万一实际光照突然变化或者负荷暴涨模型给出的调度方案根本兜不住底。两阶段鲁棒优化解决的就是这个不确定性问题。它的核心思路可以类比成打仗之前的排兵布阵第一阶段是在还不知道敌方具体行动的情况下做决策这个决策得先定下来没法等情报全部清楚了再改第二阶段则在对手行动暴露后根据实际发生的情况做调整用最小的代价来补救。对应到微网上第一阶段决策是日前阶段的机组启停、储能充放电计划这类必须在知道实际风光和负荷之前就定好的东西第二阶段决策则是在不确定性全部显现后微网通过调整机组出力、储能实际放电量来保证系统安全运行的实时调度动作。有一点需要先讲清楚鲁棒这个词在优化领域里指的不是模型跑起来不容易崩而是在最恶劣情况下系统依然能正常运行。两阶段鲁棒优化本质上是一个min-max-min形式的博弈外层最小化综合成本中层不确定性变量选择让成本最大的那个场景也就是最坏场景内层再针对这个最坏场景做最小化调整。这就和随机规划不同了随机规划给各个场景分配概率然后求期望鲁棒优化不关心概率它只盯着最坏情况兜底。刚接触这个模型的读者很容易把最坏场景理解为光伏设为0、负荷设为最大值就是一个场景。实际没这么简单因为场景里的多个不确定性变量是要满足实际约束的比如光伏出力受天气影响有上限负荷扰动范围有边界这些约束交织在一起最坏场景往往不是一个边界上的简单组合。这也是两阶段鲁棒优化程序写起来要比普通优化模型复杂得多的根本原因。2. 微网模型为什么选择它作为鲁棒优化的载体2.1 微电网的结构与核心设备集微电网之所以成为两阶段鲁棒优化的经典应用载体是因为它的设备集合天然包含了需要提前定计划和运行中需要实时调整两类决策。一个典型的中压微电网包含燃气轮机或柴油发电机、储能电池、光伏阵列、负荷以及和外部大电网的联络线。光伏出力是典型的不确定源负荷也有很强的随机波动但储能和可控机组在运行中可以快速调整出力。这类系统的调度问题可以拆成两个时间尺度。日前阶段决定机组的启停状态、储能的充放电计划、以及从网外购电的中长期计划这些决策一旦做了当天基本不会再改。日内阶段则是在实际运行中根据光伏和负荷的实测值控制器在几分钟到几十分钟内调整储能出力、柴油机组出力以及跟大电网的实时交换功率维持系统功率平衡。这里有个做程序时的常见误区把日前决策和日内决策写成两个独立的模型先算日前再把日前结果当参数丢进日内模型里优化一次。两阶段鲁棒优化是要求跨阶段一体化考虑的——日前决策定下来之后日内阶段要针对所有的可能场景尤其是在最坏场景下都可行而不是只针对某一个特定场景可行。这意味着阶段之间是嵌套关系不是串联关系。2.2 不确定性集合怎么建模在搭建两阶段鲁棒微网模型时不确定性建模是最关键的步骤。常见的方式是把光伏出力和负荷的预测值作为基准加入一个上下浮动的范围再用箱式集合来描述光伏出力P_pv P_pv_forecast ΔP_pv其中ΔP_pv的绝对值不超过预测值乘以扰动比例系数。 负荷水平P_load P_load_forecast ΔP_load同理受扰动边界约束。箱式集合的好处是简单直观参数少但缺点是它假设所有不确定源同时取到最坏值这在实际系统中过于保守。工程上更常用的是引入一个预算参数Γ来限制总的不确定度偏移量也就是所有不确定变量的归一化偏移之和不超过Γ。这个Γ也叫鲁棒控制参数、不确定度预算它的取值范围通常在0到总不确定变量个数之间。预算参数的意义在于提供了一个保守度旋钮。Γ取0时模型退化为确定性模型完全信任预测值Γ取满时模型考虑所有不确定性同时爆发的最坏情况保守度最高。实际应用中可以扫描不同的Γ值画出一条成本-保守度权衡曲线供决策者选择。这段内容实操中会直接影响对偶模型约束的复杂度我建议在写程序前先把不确定性集合的代数形式完整推导出来尤其注意预算约束的不等式方向。2.3 微网两阶段鲁棒调度模型的完整表达微网的日前-日内两阶段鲁棒调度模型可以写成如下形式第一阶段日前目标是最小化机组启停成本、储能固定调度成本、以及第二阶段对所有可能不确定场景下的期望/最大运行成本之和。约束包括机组启停逻辑约束储能充放电状态互斥约束联络线购电上限约束等。第二阶段日内针对给定的第一阶段决策和不确定变量取值调整机组出力、储能充放电功率、弃光量、切负荷量最小化即时运行成本。约束包括功率平衡约束、机组出力上下限及爬坡约束、储能SOC动态方程及容量约束、联络线功率约束。写这个模型时需要注意两个细节。第一个是储能SOC方程里带时间耦合不管第二阶段在哪个场景下SOC状态要满足递推关系第二个是我第一版程序里很容易漏掉的第二阶段约束中的不确定性变量可以出现在功率平衡方程里但不可以出现在第一阶段约束里否则模型就变成了完全耦合的无法用标准的分解算法求解。还有一个逻辑需要明确如果第二阶段模型不可行怎么办实际工程处理方式是引入失负荷变量或弃光变量作为松弛变量对应非常高的惩罚系数。这种做法虽然让模型任何决策都能兜底但惩罚系数设置不合理时会出现假的可行解——惩罚成本比购电成本低模型宁可切负荷也不去买电这个问题我在第5节会再展开讲。3. CCG算法求解两阶段鲁棒优化的核心利器3.1 主问题与子问题的分解逻辑CCGColumn-and-Constraint Generation列与约束生成算法是目前实现两阶段鲁棒优化的主流方法。它不像Benders分解那样通过不断添加割平面来逼近而是每一步迭代都把一个最坏场景的具体实现当成新的列和约束加入主问题使得主问题的规模逐步扩展逼近原问题的最优解。标准的算法流程是这样初始化给定一个不确定性场景通常取预测值或边界值。主问题在已经发现的有限个场景集合上求解第一阶段决策和对应的总成本得到可行解和成本下界LB。子问题固定第一阶段决策求解内层最小化和外层最大化问题找到当前决策下最坏的不确定性实现并把最坏成本作为上界UB。收敛判断如果UB和LB之间的相对间隙Gap小于设定阈值比如0.01则终止迭代输出最优决策否则把新的最坏场景对应的变量和约束加入主问题回到第2步。在程序实现过程中主问题的规模会随着迭代次数增加而变大。每轮迭代都加入一组新的第二阶段变量和约束这些变量和约束对应不同的不确定性场景实现。做仿真时常看到有人在迭代30轮之后主问题已经有上万条约束求解速度明显变慢这是正常的。合理的Gap设置可以避免无意义的多余迭代我一般取0.01工程上0.05也够用。3.2 子问题求解双层结构不能直接求解子问题最麻烦的地方在于它是一个max-min双层结构外层不确定性变量在找最坏场景内层运行变量在做最小化调度。绝大多数商业求解器比如Gurobi、CPLEX不支持直接求解这种双层模型所以要做转换。标准做法是利用强对偶理论或KKT条件把内层的最小化问题对偶成一个最大化问题这样max-min就变成了一个单层的max-max可以直接合并成最大化问题。对偶转换有严格的条件内层问题是线性的或者凸的、满足强对偶条件。这也是为什么很多学术论文里的微网模型要刻意简化——储能和机组启停在第二阶段通常是固定值或线性化表示的目的就是为了保持第二阶段问题线性。从实操角度讲我建议在第一天写代码的时候就把第二阶段模型严格限制为线性规划。不要一开始就上混合整数非线性否则对偶推导和求解器配置的复杂度会让你怀疑人生。等线性版本跑通了再逐步放宽模型假设。3.3 对偶子问题中的KKT条件和大M法除了纯对偶方式还有一种常见的转化思路是直接在子问题里引入KKT条件把内层最小化问题的最优性条件作为约束加到子问题中。KKT条件包括原始可行性约束、对偶可行性约束、互补松弛条件、拉格朗日函数梯度为零的条件。其中互补松弛条件是非线性约束没法直接丢给Gurobi这类求解器处理。工程上的做法是用大M法将互补松弛条件线性化引入一个二进制变量配合一个足够大的M常数把两个约束的乘积关系拆成两组线性约束。大M法的难点在于M的选取。M选小了会截掉可行域选大了会造成数值病态。实操技巧是根据模型参数的取值范围把M设置成可能取到的最大绝对值乘上10到100倍。比如某个对偶变量理论上最多取到1那M取100就够。不过这个取值范围推导需要结合具体约束来分析每个问题不一样。我在论文复现中踩过一个具体坑一开始对A、B两组互补条件用了同一个M值结果子问题松弛出了一些不合理的对偶变量取值。后来把M分开设置一组取100一组取500问题就消失了。求解器的数值容差在这种问题上很敏感大M的值宁大勿小但也不要大到离谱。还有一点值得说如果用的是YALMIP或CVX这类建模工具它们内置的对偶化功能在某些场景下会自动处理这类问题但生成的模型形式不够直观调试困难。如果我需要在论文里解释清楚求解逻辑更推荐手动推导对偶形式再显式地建模子问题。3.4 为什么我推荐先用商用求解器而不是开源求解器两阶段鲁棒优化程序里主问题是混合整数规划MIP子问题是带对偶变量的线性规划LP。主问题的求解效率直接决定了整个算法的迭代速度而像CBC、GLPK这类开源求解器处理大规模MIP的能力明显弱于Gurobi或CPLEX。学术用途的Gurobi许可证通常免费所以优先推荐它。求解器配置上值得注意的有几点。一是MIPGap参数主问题的求解精度不必太高MIPGap设在0.001到0.0001之间就行我在快速验证时常用0.0001二是在子问题求解后要检查求解状态如果子问题无界通常说明对偶推导或者大M设置出了问题需要从模型层面排查三是Gurobi在多线程下的表现要远好于单线程但在CCG主问题规模不大时开8个线程以上的边际收益递减不必盲目堆线程数。4. 从零搭建两阶段鲁棒优化程序核心模块拆解4.1 代码框架模块划分我习惯把一套两阶段鲁棒优化程序分成四个模块数据输入模块读取系统参数、设备参数、预测曲线、不确定性集合参数统一放到配置类里。主问题构建模块负责创建主问题模型、添加第一阶段变量和约束、维护不断膨胀的场景集合对应的变量与约束。子问题构建模块负责构建对偶化后的单层子问题输入第一阶段的固定决策输出最坏场景以及对应的第二阶段成本。CCG循环控制模块负责初始化场景、循环调用主问题与子问题、计算Gap、判断收敛、输出结果。这种模块化设计的好处是每个部分都可以独立调试。尤其子问题和主问题分开写之后你可以先用一个假的第一阶段决策来测试子问题是否能正常求解而不是等到整套算法跑完才发现问题是出在子问题构建上。4.2 主问题构建的伪代码骨架def build_master_problem(config, scenarios): 构建CCG主问题 mp Model(Master_Problem) # 第一阶段变量 x_start mp.addVars(n_generator, vtypeGRB.BINARY, namestart) x_soc mp.addVars(n_battery, T, namesoc) # 储能SOC计划 # 对每个已发现的场景添加第二阶段变量 for s, scenario in enumerate(scenarios): y_g mp.addVars(n_generator, T, namefy_gen_{s}) y_pgrid mp.addVars(T, namefy_grid_{s}) y_pbat mp.addVars(n_battery, T, namefy_bat_{s}) # 引入该场景对应的耦合约束 for t in range(T): mp.addConstr( sum(y_g[g, t] for g in range(n_generator)) y_pgrid[t] sum(y_pbat[b, t] for b in range(n_battery)) scenario[load][t], namefbalance_{s}_{t} ) # 储能SOC与日前的关联 for t in range(1, T): mp.addConstr(y_soc[s][t] y_soc[s][t-1] y_pbat[s][t] * dt) mp.setObjective(...) # 第一阶段成本 最大场景成本加权 return mp代码片段省略了储能SOC变量在不同场景间如何关联的细节但大致流程就是这样。每轮迭代在新加入的场景索引s下生成一组新的第二阶段变量这些变量的约束参数可以不同因为场景不同但第一阶段变量是共享的。这里我需要强调一个关键点不同场景的第二阶段变量之间不能有任何交叉约束否则会把每个场景单独可行的要求错误地强化成所有场景同时满足同一个变量取值的更强条件导致结果过度保守。我见过有人把储能SOC场景间写成同一个变量结果模型从鲁棒变成了拍脑袋运行成本特别高。4.3 子问题构建与对偶化步骤子问题代码的核心工作在于把第二阶段最小化问题转成对偶形式。假设第二阶段问题是min c^T y受A(x) y B u b约束等引入对偶变量λ后对偶问题是max b^T λ - (某部分关联x的项)受A^T λ cλ 0约束。实际写代码时我建议先用纸笔把对偶问题完整推导出来再逐条写到代码里。常见的推导错误包括等式约束对应的对偶变量没有自由符号限制、目标函数中常数项漏掉、对偶变量的符号方向写反。这些错误在数值上只会在Gap不收敛时才暴露出来调试起来非常痛苦。我把这部分的推导经验总结成三条第二阶段目标如果是min cost对偶后的目标就是max something符号要检查两遍。功率平衡等式对应的对偶变量是没有符号限制的free variable不要写成非负约束。不确定性变量出现在约束右侧时其对偶项会连带到目标函数中这个环节最容易被忽略。4.4 多面体不确定性集合的对偶表达如果不确定性集合是多面体比如带预算约束的箱式集合在子问题里最大化部分变成对不确定变量和原对偶变量的乘积项求最大值。这是个双线性项没法直接线性求解。处理方式是在子问题里做一次线性化因为不确定性变量的可行域是多面体最大化一个双线性项等价于在极值点取最优。如果不确定性变量只出现在约束右侧线性化后可以转化为一组针对顶点场景的枚举或等价线性表达式。实操中如果不确定性集合比较小比如只有光伏和负荷两个不确定源直接枚举顶点也是一种简单可行的办法虽然理论效率不如全对偶方式。我在小规模测试模型里直接枚举4到8个顶点效果相当稳定推荐新手先用这个方式验证整体框架再过渡到完整的对偶子问题。5. 实操过程中我踩过的坑与调优心得5.1 子问题不可行或对偶无界这是最常见的问题。我第一版模型在跑子问题时求解器直接报Infeasible。排查方向有两个第一第二步可行性没问题可能是第一步决策太激进比如储能SOC越限了模型本身没有留裕量第二更隐蔽的问题是子问题里某些约束变量没接上导致变量完全自由产生了一个看似不可行其实是模型的逻辑漏洞。调试办法是在子问题脚本里把第一阶段决策变量替换为一个人工构造的可行解比如所有机组出力取预测负荷的50%、储能出力为0。如果这样还不可行那说明模型本身有约束冲突和鲁棒算法无关。这个技巧在找bug的时候极其高效。5.2 惩罚系数陷阱前面提到的失负荷惩罚系数我一开始取了很高的值10000元/千瓦时结果主问题的优化器总倾向于花这个代价去调整储能计划反而不去利用更现实的购电空间。后来我做了扫描把惩罚系数当成超参数从1000到50000逐步试验观察最优成本曲线。当系数大到一定程度后成本不再变化那个点附近的取值才是合适的。工程里面有个经验值可以参考失负荷惩罚系数至少要高于最贵的机组发电成本10倍以上但不能高到扭曲模型数值稳定性比如超过1e6。5.3 Gap收敛慢的问题如果CCG迭代20轮以上Gap还在3%以上徘徊优先怀疑两件事第一个是主问题MIPGap设置太松导致加进子问题里的决策x不够准最坏场景判断失真第二个是子问题对偶模型构建有微小错误导致UB偏小或LB偏大。我在一个复现案例里发现把主问题MIPGap从1e-4调到1e-6后Gap在8轮后就降到0.1%以下。所以要分清楚CCG外层Gap与求解器内层MIPGap不是一回事前者是算法迭代的收敛判据后者是求解器求解主问题的精度。两层精度要求要协调。5.4 场景遗漏与初始化场景的选择初始化场景如果直接取预测值前几轮迭代时主问题相对乐观子问题暴露出更坏场景后Gap会先上升再下降这是正常现象。如果初始化直接取顶点极值场景迭代次数可能更少但这取决于不确定性集合的形状。我自己的经验是先用预测场景初始化跑一遍观察最坏场景出现在哪几个维度再手动构造一个极值场景作为新的初始场景可以加快收敛不到10轮。小规模模型可以忽略这个调优但大规模比如52周安全约束机组组合问题省一轮迭代能省不少时间。5.5 从论文仿真到工程落地的差距论文里常用的微网模型一般只做1小时或15分钟的调度阶数少鲁棒优化跑起来很快。实际工程问题往往是几千个节点、8760小时潮流约束的联合优化场景集合里的每一条约束对应的是完整的潮流方程计算量立刻膨胀。这个阶段需要用场景缩减技术或者并行计算来加速。场景缩减可以用经典的快速前向选择算法把所有历史场景聚类成有代表性的若干场景主问题里的model.update()过程也可以设置成懒更新。不过算法细节每套系统都不太一样这里就不展开讲了。如果你只是复现论文里的微网模型我强烈建议先从3个节点、24小时、两台机组这类迷你规模开始先把代码框架调通再逐步放大。直接上大规模模型会让代码里同时出现对偶推导问题、求解器配置问题、算法收敛问题三个问题纠缠在一起几乎没法调试。这套程序写完之后稍微改改数据文件就能迁移到配电网、综合能源系统、虚拟电厂这类场景。两阶段鲁棒优化本身不局限在微网它的核心思想——先用保守的日前计划保底再用灵活的日内调度兜底——在大多数存在预测不确定性的调度问题里都能用上。
返回列表