ARTICLE DETAIL

资讯详情

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

分布式鲁棒优化微电网单元分配:Python复现全攻略

分布式鲁棒优化微电网单元分配:Python复现全攻略 1. 项目缘起为什么我选择复现这个“分布式鲁棒优化微电网单元分配”算法先说个背景。做微电网调度或者新能源方向的研究生这两年应该没少跟“分布式鲁棒优化”打交道尤其是分布式鲁棒优化这个关键词几乎成了高水平论文的标配。这个词很容易劝退初学者——既涉及多智能体协调又涉及分布鲁棒听起来像两个大坑叠在一起。但如果你把一个微电网群的日前调度问题抽象出来你会发现它本质上就是“单元分配”加“不确定性处理”哪些分布式电源要开机、哪些储能要充放、各单元出力怎么分配、微电网之间交易多少电量。我当时复现这个项目起因是组内需要一套可复现的基准算法用来跟改进方法做对比。翻了很多篇文章要么只有数学推导没有公开代码要么代码只是跑了一个简化玩具案例离“按高水平文章复现”差得远。最后决定自己动手把目标定成基于 Python 写一套源代码覆盖数据生成、模糊集构建、CCG列与约束生成外层迭代、ADMM 内层分布式求解、结果可视化全过程。本文并不打算贴一份完整代码然后让你直接复制而是把整个复现过程的思考链、数学模型、关键代码骨架以及我踩过的坑讲清楚让你拿到任何一篇同类文章时都能快速搭出可运行的框架。这个内容适合什么样的人第一类是刚接触鲁棒优化、想搞懂代码怎么跟公式对应的研究生很多论文公式符号跳得厉害对着代码看反而容易理解第二类是工业界做园区微网或智能配电网能量管理的工程师想评估这类方法是否值得落地第三类是把论文复现当作学习方式的人想学习高水平代码的工程组织能力。我复现时的设定是这样一个多微电网系统每个微网内部包含光伏、风机、储能、微型燃气轮机以及不可控负荷微网之间通过联络线相连可以交换功率整体目标是让全系统运行成本最小化。微网内部单元众多“单元分配”就是对这些设备做日前启停方案和出力计划分配。光照、风速、负荷都有预测误差所以决策必须在不确定性下做这也是引入分布式鲁棒优化的原因。1.1 微电网单元分配问题到底在做什么单元分配是电力系统里很老的一个问题英文叫 Unit Commitment我在博文里标题里的“单元分配”指的就是这个而不是什么用户资源分配。工业上的微电网管理系统本质上每天都要做一遍根据第二天的负荷和新能源预测决定哪些机组开起来、开几台、储能是充还是放、要不要向外部电网购电或售电、每个微网之间交换多少功率。决策维度包括两类变量一类是 0-1 离散变量代表机组启停状态一类是连续变量代表各时刻有功功率、储能充放电速率、与电网交互功率等。如果忽略不确定性这就是一个混合整数线性规划MILP用 Gurobi 或者 Cplex 很快就能求出来。但实际问题恰恰出在预测上。光伏预测第二天可能多云转晴偏差 30% 都不罕见负荷预测相对稳一些但也有 5%-10% 的误差。确定性的结果通常是满发超发或者缺电补购要么浪费弃光要么考核罚款极端情况下可能造成频率越限。所以从工程上看单元分配必须跟不确定性结合。传统随机规划假设不确定量的概率分布已知然后对多组场景求期望成本最优。这个假设在实际系统里站不住脚——我们只有历史预测误差数据真实的概率分布永远没法知道。这就是分布式鲁棒优化登场的根本原因。1.2 分布式鲁棒优化和“分布式求解”的两层含义我相信很多人第一次看到“分布式鲁棒优化微电网单元分配”这个题目会觉得这里的“分布式”说的是求解算法的分布式也就是多个微网之间不依赖中央控制、各自迭代交换边界信息。其实这个题目里的“分布式”有两层含义第一层物理系统本身是分布式电源模型天然包含多个自治微网主体第二层求解算法上采用分布式优化框架不是把全系统建模成一个单一大优化问题直接求解。这两层含义的取舍会影响整篇代码的结构。如果只研究一个微电网内部调度分布式指的是可再生能源分散接入这时候“分布式鲁棒优化”主要指在模糊集下做两层决策模型依然可以整体求解。但如果研究对象是微电网群我们就要考虑隐私和独立性每个微网不想把内部拓扑、负荷细节上传给某个中央调度中心只愿意交换联络线功率信息。我复现的时候选择了第二种设定因为这样代码的可扩展性更好也更能体现“分布式”三个字的工程价值。这直接决定了代码必须拆成两部分每个微网内部的局部优化模型以及微网之间的协调更新机制。后面第四部分我会具体讲这个架构。2. 数学模型梳理从目标函数到Wasserstein模糊集复现任何一篇高水平文章第一步都绕不开“把公式吃透”。我建议你先把论文里的数学符号自己在白纸上复写一遍每个变量的上下标都标清楚然后再开始写代码否则后期调试的时候你会经常卡在维度对不上这种低级错误上。2.1 两阶段DRO的标准形式分布式鲁棒优化的核心思想是我们不知道真实概率分布但可以构造一个模糊集——一个包含所有可能概率分布的集合然后在这个集合里找最坏情况下的最优决策。这个想法就像买保险你不是为某一种天气情况准备方案而是为最坏的那类天气情况做准备。相比传统鲁棒优化它不限制具体场景的区间范围而是限制分布之间的“距离”所以不会因为某一个极端点而把整个解搞得太保守。两阶段结构对应实际物理过程。第一阶段决策是日前计划在知道准确的新能源出力之前就要确定启停状态、购电基线等第二阶段是实时调整光伏实际出力出来了再通过储能调节、燃气轮机微调来弥补偏差。目标函数写成$$ \min_{x \in X} \left{ c^T x \sup_{P \in \mathcal{P}} \mathbb{E}_P [Q(x, \xi)] \right} $$第一项是日前启停和运行成本第二项是在所有可能概率分布 $P$ 中找期望调整成本最大的那个——也就是最坏情况分布下期望成本。$Q(x, \xi)$ 是给定第一阶段决策 $x$ 和不确定实现 $\xi$ 后的第二阶段最优成本它自己又是一个向下优化的函数。这个形式在所有 DRO 文章里几乎都一样区别主要在模糊集 $\mathcal{P}$ 的构造方式以及第二阶段约束的复杂度。2.2 数据驱动模糊集的设计逻辑模糊集的构建方式决定了这篇文章的“含金量”。我复现时用了基于 Wasserstein 距离的模糊集这是近几年高水平文章里最常见的做法。它的定义是以历史误差数据的经验分布为中心把所有跟这个经验分布的 Wasserstein 距离不超过半径 $r$ 的分布都纳入模糊集。Wasserstein 距离可以理解为“把一个概率分布搬运成另一个概率分布所需的最小代价”。为什么选它而不是 KL 散度或者矩约束两个原因第一Wasserstein 距离对分布的“位置偏差”非常敏感换句话说它允许分布整体移动偏离一点这符合新能源预测误差的实际情况——天气系统预报偏移会导致整段出力曲线平移而不是单纯方差变大第二在有限样本下Wasserstein 模糊集的样本复杂度有理论保证半径可以按样本数量推导出合理上界这给了实际调参一个可靠的锚点。代码层面的关键是模糊集半径的选择。我在复现时采用了一个工程化的估计方式$$ r C \cdot \sqrt{\frac{\log(1/\beta)}{N}} $$其中 $N$ 是历史误差样本数量$\beta$ 是置信水平$C$ 是一个与数据尺度相关的常数可以用训练集上的 Wasserstein 距离交叉验证去标定。这个公式基于有限的样本复杂度理论具体数值在不同文章里略有差别但量级关系是一致的样本越多半径越小决策越不保守置信水平越高半径越大系统越安全。构建模糊集的具体步骤如下收集历史预测值与实际值的差值得到误差序列 ${\hat{\xi}_1, \hat{\xi}_2, ..., \hat{\xi}_N}$给每个历史误差样本赋权重 $1/N$形成经验分布以经验分布为球心按上述公式计算半径 $r$模糊集定义为所有与经验分布 Wasserstein 距离不超过 $r$ 的分布集合。实际代码中这个模糊集不会显式枚举所有分布而是在重构阶段被转化为有限维约束。这是 Wasserstein 模糊集最方便的一点——对偶变换后第二阶段期望可以由一组离散场景的加权组合近似表达原本“所有分布”的无限维优化变成有限场景的凸优化问题。这个性质我在 CCG 求解时反复用到。2.3 约束条件的工程取舍数学模型必须包含实际物理约束但不需要面面俱到。我整理了一套适合复现的最小约束集既保留问题本质又避免建模过度复杂导致求解困难约束类型数学表达工程含义复现取舍功率平衡(负载 充电 光伏 风电 燃气机 放电 联络线)每小时电量守恒必需若忽略则结果无意义机组容量出力必须处于最小技术出力与额定容量之间发电机物理限制必需爬坡约束相邻两小时出力变化不超过上限机组响应能力有限保留用于验证算法处理耦合约束的能力储能SOC荷电状态在安全区间内变化电池过充过放保护必需同时要加终端电量约束启停时间约束开机后不能立刻停机机组热力学要求第一版可省略简化代码联络线容量微网间交换功率不超过线路限额物理传输约束必需这是 ADMM 耦合的核心我见过不少复现代码把储能部分直接简化成一个能量桶只约束充放电功率和容量SOC 不建模。这会让结果完全不真实——从论文图和表格上的数据是看不出来的但一旦你把 SOC 曲线画出来就会发现储能每天几乎只充放一次跟实际运行完全不符。复现时该加约束还是得加这些约束不会让模型变到不可解只是多几行代码的事。3. 求解算法选型CCG与ADMM如何协作模型建立之后最关键的决策就是选择求解算法。这一点很多人容易想当然觉得既然是优化问题建模后丢给求解器就行。但你如果只是“建模后丢给求解器”标题里的“分布式”三个字就丧失意义了因为你只是在写一个 MILP 而已算法没有任何创新价值。高水平文章的贡献点之一就是给出针对不能整体求解场景的分布式算法复现的核心也在于此。3.1 为什么不用一层求解器直接跑这里先把思路捋清楚。两阶段 DRO 如果整体求解可以转成大规模 MILP假设场景有限而离散化。直接丢给商用求解器并非不能解但有三个问题第一场景数量是外部不确定性集合离散化后产生的为了逼近最坏情况分布需要非常多场景规模动辄上万矩阵维度会很大第二微网之间的联络线耦合会让系数矩阵的稀疏性变得很差求解器预处理阶段就开始变慢第三从算法贡献角度看直接整体求解无法体现“分布式”的思想也就没有真正复现出文章的方法。所以我选了“CCG 嵌套 ADMM”的组合外层 CCG 负责把不确定空间中所有可能的最坏场景逐步“拉”进主问题内层 ADMM 负责处理多个微网之间的联络线功率耦合。这个拆分跟问题本身的物理结构是匹配的不确定性和空间分布式各由一层算法对应逻辑非常干净。3.2 外层CCG主从迭代CCG 的原理其实不复杂学名“列与约束生成”。它跟 Benders 分解的思路类似通过不断求解主问题和子问题来逼近最优解。主问题是去掉“所有分布”之后的有限场景优化子问题是固定第一阶段决策后去找最坏情况下的不确定性分布。每次求解子问题得到一个最坏场景把它及其对应的约束加回主问题然后重新求解主问题循环往复直到上下界间隙收敛。算法流程如下初始化给定初始场景集合清空最优性间隙变量求解主问题在当前场景集合下求最优决策得到下界 $LB$固定 $x$求解子问题在所有可能的分布中找到最坏情况期望成本得到上界 $UB$如果 $UB - LB \le \epsilon$ 则退出否则将子问题对应的最坏场景作为新列加入主问题转向第 2 步。初始迭代中下界对应的目标值偏低随着场景不断加入可行域变紧下界逐步抬升逼近上界。需要注意CCG 里的子问题本身是一个 min-max 结构或 max-min 结构。我们在代码中会对内层做对偶变换把 max-min 化为一个单层 LP但需要注意对偶间隙的存在条件满足 Slater 条件的情况下可以直接对偶。这也是为什么我在前面强调模型中所有约束要是线性的否则这一步会非常麻烦。3.3 内层ADMM处理联络线功率耦合主问题每一次迭代都是一个确定性的多微网 MIQP/MILP。如果每个微网独立求解它们各自内部的功率平衡不一定满足因为联络线功率怎么分配没有协调。此时引入 ADMM。ADMM 的思想不复杂每个微网先各自基于本地信息求解自己的优化问题得到一个期望的交换功率然后协调器把这些期望值做平均作为全局一致的交换功率参考再更新拉格朗日乘子重复迭代。更具体一点每个微网 $i$ 更新自己的决策变量$$ p_i^{k1} \arg\min_{p_i} \left{ f_i(p_i) \frac{\rho}{2}\left| p_i - z^k u^k \right|_2^2 \right} $$然后全局更新$$ z^{k1} \frac{1}{M}\sum_{i1}^M \left( p_i^{k1} u^k \right) $$最后更新对偶变量$$ u^{k1} u^k p_i^{k1} - z^{k1} $$这里面最关键的角色是“参考交换功率” $z$它的作用就是让各个微网之间最终达成一致。实际运行时迭代几次之后微网之间交换功率的偏差会收敛到接近零。收敛判据通常用原始残差和对偶残差两个指标双重判断。原始残差是交换功率实际值与参考值的差对偶残差是相邻两次迭代决策变量的变化量两者都小于阈值才认为收敛。为什么选 ADMM 而不是别的分布式算法因为它的目标函数可以是混合整数问题经过连续松弛之后的凸函数工程实现简单对脚本参数也不是特别敏感而且天然契合“Pyomo 局部求解 numpy 外循环约束更新”这种可读性强的代码风格。4. Python代码架构目录结构、数据流与核心模块实现数学部分梳理清楚之后代码怎么写就非常有数了。我按照“数据层—模型层—算法层—可视化层”来做代码架构这是复现代码里我认为最值得借鉴的部分也是工程效率提升的主要来源。4.1 工程目录与模块划分我最终采用的目录结构如下尽量简化避免新人眼花缭乱microgrid_dro/ ├── data/ │ ├── load_profile.csv │ ├── pv_profile.csv │ ├── wind_profile.csv │ └── forecast_error.npy ├── src/ │ ├── __init__.py │ ├── data_loader.py │ ├── scenario_generator.py │ ├── dro_model.py │ ├── ccg_solver.py │ ├── admm_core.py │ └── utils.py └── main.py这个结构的核心思路是各模块职责单一。data_loader.py专门负责读 CSV 数据scenario_generator.py负责把历史误差序列转化为多场景数据dro_model.py负责将模糊集、目标函数、约束表达成求解器可用的形式这里我用了 Pyomoccg_solver.py是外层迭代逻辑admm_core.py是内层分布式协调模块utils.py放可视化、指标计算、日志输出等辅助函数。这就保证了如果你想改某一个策略不用动其他模块。4.2 场景生成与数据预处理场景生成是复现 DRO 的一个重头戏。在数据的预测误差序列中最直接的场景生成方法是自助抽样bootstrap也就是从历史误差中随机抽取若干条样本作为场景。可以这样实现import numpy as np from scipy.stats import gaussian_kde def bootstrap_scenarios(history_errors, n_scen100, seed42): rng np.random.default_rng(seed) n_hist len(history_errors) idx rng.integers(0, n_hist, sizen_scen) scenarios history_errors[idx] prob np.full(n_scen, 1.0 / n_scen) return scenarios, prob这只是最简单的情况用经验分布采样每个样本的权重相等。实践中你可能会觉得直接用历史误差样条过于粗糙因为极端情况出现得太少。这时可以引入核密度估计KDE对历史误差分布做平滑再进行采样def kde_scenarios(history_errors, n_scen100, seed42): rng np.random.default_rng(seed) kde gaussian_kde(history_errors.T) scenarios kde.resample(n_scen, seedseed).T prob np.full(n_scen, 1.0 / n_scen) return scenarios, probKDE 方式生成的场景会包含历史数据中没出现过、但大概率会出现的插值点相当于把样本空间填得更加密集。注意 KDE 的带宽参数会影响生成的场景偏离程度过大则发散严重增加模糊集处理的难度过小等于还原回经验分布。我实践中会先用 10 折交叉验证确定带宽再正式生成场景这一点很多教程不会告诉你。场景生成完还要做一件事场景削减scenario reduction否则 100 个场景对 CCG 外层迭代来说足够了但如果是 500 个场景子问题求解速度会显著变慢。最常用的方法是快速前向选择法逐个选取使得原分布与削减后分布的 Kantorovich 距离最小的子集。这个思路的实现基于距离矩阵计算量不大适合日常调试。4.3 Wasserstein半径与模糊集参数半径 $r$ 的选取是 DRO 的资本所在。代码核心就是计算一个经验半径再作为传递参数进入模型def wasserstein_radius(n_samples, delta0.05, diameter10.0): Compute a>def ccg_solve(master_model, sub_problem, x_var, xi_vars, tol1e-4, max_iter20): lb -np.inf ub np.inf scenario_pool initial_scenarios[:] for k in range(max_iter): # (1) Solve master with current scenario pool master_model.scenarios scenario_pool solver.solve(master_model) lb master_model.objective.expr() # (2) Fix first-stage decision x_fixed extract_x(master_model) # (3) Solve sub-problem to find worst-case scenario worst_obj, new_scenario solve_worst_case(sub_problem, x_fixed) ub min(ub, worst_obj) # (4) Check tolerance if abs(ub - lb) / abs(lb) tol: break # (5) Add worst-case scenario into pool scenario_pool.append(new_scenario) return x_fixed, lb, ub这里的solve_worst_case是子问题需要在固定 x 的情况下最大化第二阶段成本对应的对偶值然后将该对偶解对应的原场景取出来。注意子问题在实现时需要把原 market 复位不能直接沿用主问题的变量空间否则容易遭遇求解器内存中残留约束的问题。ADMM 核心更新用纯 numpy 就可以实现关键的协调部分不依赖 Pyomodef admm(z0, rho, local_solver_callback, n_agents3, max_iter100, tol1e-3): z np.copy(z0) u np.zeros_like(z0) history [] for k in range(max_iter): p_next [] for i in range(n_agents): p_i local_solver_callback(i, z, u, rho) p_next.append(p_i) p_next np.stack(p_next, axis0) z_next p_next.mean(axis0) u.mean(axis0) u u (p_next - z_next[None, :]) r_prim np.linalg.norm(p_next - z_next[None, :]) r_dual np.linalg.norm(z_next - z) history.append(max(r_prim, r_dual)) if max(r_prim, r_dual) tol: break z z_next return p_next, z, historylocal_solver_callback这个函数返回每个微网在当前参考功率和前一轮对偶乘子下的最优决策结果。对每个微网来说其实就是在本地目标函数上增加一个二次惩罚项def local_objective_with_penalty(model, z_local, u_local, rho): # original operating cost expr sum(...) # fuel, startup, battery wear, etc. # consensus penalty on tie-line power expr sum( (rho / 2) * (model.p_tie[t] - z_local[t] u_local[t])**2 for t in model.time_horizon ) return expr注意这里如果本地问题包含 0-1 变量ADMM 收敛性理论上没有保证。我在第五部分会详细讲这个问题怎么处理。5. 复现中的五个高频坑从求解器内存爆炸到整数变量冲突这一段是本篇博文的重头戏也是我觉得真正值得收藏的部分。这些坑我基本都踩过有些甚至调了一周写出来希望你能绕开。5.1 场景数量膨胀导致的求解器内存爆炸CCG 每次迭代都会增加一个新的最坏场景每个场景都会往主问题里增加一组变量和约束。如果你的第二阶段模型比较精细有 24 个时段、20 台机组、5 个储能那么每个场景带来的额外变量就有上千个。迭代 20 轮之后主问题的规模会让人绝望——Pyomo 建模速度明显变慢求解器耗时从秒级变成分钟级再变成几十分钟。我用过的解决方法是给场景池加上“上限策略”只保留最近 T 轮迭代加入的场景。因为 CCG 的收敛逻辑里最新的最坏场景对解的改进贡献最大而早期加入的场景可能在后续迭代中已经不是最紧的了。保守的做法是每 5 轮清理一次只保留本轮起始的初始场景和最近 5 轮的最坏场景。这样主问题规模会稳定在一个可控范围。代价是理论上可能丢失全局最优性保证但实际测试中只要清理间隔不是太小对最终解的影响在 0.5% 以内。5.2 整数变量遇上ADMM先松弛再回代这是多微网分布式调度复现里最深的一个坑。ADMM 对混合整数问题的收敛性目前理论上是没有普遍保证的。你说你每个微网内部有机组启停变量直接用 ADMM 迭代乘子更新几次之后整数变量开始来回跳残差不收敛这个问题版上已经问过无数遍了。我实践中的方案是“分层决策”把单元分配拆成两个阶段拿 ADMM 迭代的变量是连续量——联络线交换功率和各机组有功出力机组启停状态这种 0-1 变量在每次 ADMM 迭代固定当前参考功率之后、由本地求解器一并解出。也就是说在第 k 次 ADMM 迭代时本地求解的是一个 MILP求解器在考虑当前惩罚项的前提下自行决定哪些机组开或关然后把开机的机组出力作为连续结果返回到 ADMM 的全局更新。这样 ADMM 公式里只涉及连续变量的一致性更新公式0-1 变量只影响本地目标函数迭代过程整体可以收敛到你期望的工程精度亲测有效。5.3 ρ参数的选择与收敛判据ADMM 的罚参数 ρ 直接影响收敛速度和解的质量。取太小原始残差下降慢取太大解在质量上偏向参考值目标函数的最优性受损。我习惯的做法是动态调整初始 ρ1.0按微网系统容量归一化比如总负荷峰值的 0.1 倍每 10 次迭代观察原始残差和对偶残差的比值如果原始残差一直比对偶残差大超过 10 倍就乘以 2反过来如果对偶残差持续偏大则除以 2收敛判据不要只用一个残差建议同时满足原始残差 1e-3 和对偶残差 1e-3 且连续 5 轮不反弹。实际的程序里当系统规模比较大的时候残差阈值过大迭代会过早停止出现微网间交换功率不一致的情况阈值过小迭代多耗的时间又让人心疼。我用 1e-3 作为初始值结合可视化残差曲线来判断是否需要进一步收紧。5.4 开源求解器与商业求解器的差距论文里的实验基本默认使用 Gurobi 或 Cplex教育许可对高校用户免费但你要出去跑真实项目不一定有许可。开源求解器里MILP 部分可以选 HiGHS这是目前开源里比较靠谱的一个或者 CBC 作为 Pyomo 的 backendLP/QP 部分完全没问题。SCIP 的功能更全但安装配置对新手不友好。在我这个复现项目里主问题的 MILP 用 HiGHS 求解子问题里对偶后的 LP 也用 HiGHS整体求解时间跟 Gurobi 相比大概慢 3-5 倍还在可接受范围。如果你的场景量特别大开源求解器优化效率的差距会被放大这时候建议先做场景削减把规模降下来再跑。5.5 结果对不上论文时的排查顺序复现结果跟原文数字不一致几乎是必然的只要量级和趋势对得上就算成功。如果完全对不上我的排查顺序是先检查数据归一化。文章里的系统参数往往用标幺值你用自己的真实单位建模仿真成本数值当然不可能一致再检查模糊集半径量纲和转化系数。这是最容易错的地方多比较几个场景下子问题的最坏情况期望成本是否随半径单调变化检查 CCG 是否在少数几个场景上反复振荡。如果上下界长期不收敛说明场景代表性不足或者模糊集半径太大最后检查 ADMM 的收敛判据。如果只用了原始残差一个标准收敛后可能对应解在可行域边界上来回横跳。6. 实验结果如何验证收敛曲线、成本对比与调度方案的合理性代码跑通之后还需要把结果验证做扎实。验证环节本身也是高水平复现的灵魂通用结论就是一句话判断一个复现代码价值的标准是你能否画出一组可信的收敛曲线、完整的成本对比表和符合物理直觉的调度方案图。6.1 需要对比的基线复现代码的价值在很大程度上体现在对比分析里。完整情况下应该在同一个测试数据上对比三类方案确定性优化方案预测等于实际忽略误差传统盒式鲁棒优化方案不确定集取预测误差的上下界本文的分布式鲁棒优化方案。从成本和资源配置的角度来看确定性方案的成本最低但是在最坏场景下的实际成本往往最高属于“看着便宜用着贵”盒式鲁棒方案的成本最高因为它对每个时刻的误差都做了极端的上下界假设解会过于保守分布式鲁棒方案居中它的优势在于把“最坏情况分布”而不是“每个时刻最坏值”放进了目标函数这个区分是很本质的。我在实际运行过程中最有代表性的观察是分布式鲁棒优化的储能充放电曲线比盒式鲁棒方案更平滑原因是盒式方案里每个时段都可能出现极端场景储能需要为各种极端情况预留双向调节空间结果就是储能的 SOC 曲线抖动非常厉害而 DRO 方案可以把这种极端情况分布化SOC 曲线表现接近于真实调度需求。6.2 收敛性与结果的判断准则具体到收敛曲线CCG 的上下界曲线应该呈现逐步逼近的态势整个过程大致在 10-25 次迭代内收敛。如果你看到下界下降或上界上升先不要慌CCG 在少数迭代内出现这种“回退”是数值噪声引起的只要整体趋势朝中间汇聚就问题不大。判断复现成功与否我一般用三条准则可复现性换随机种子后成本波动不超过 2%如果波动大说明场景数太少可解释性最坏情况场景应当落在误差数据的边界附近而不是某几个异常极端数据上否则说明模糊集半径偏大符合物理直觉光伏大的时段燃气轮机出力降低储能充电增加夜间光伏为 0 时储能放电满足负荷这条是底线。6.3 可视化与数据导出最后是可视化部分。代码里我预留了三个基础画图接口def plot_convergence(ccg_history, admm_history, save_path): ... def plot_generation(schedule_df, save_path): ... def plot_soc(soc_df, save_path): ...第一个画上下界收敛曲线判断迭代是否平稳第二个画各机组的出力堆叠图验证调度方案的合理性第三个画储能 SOC 曲线看储能调度是否过于频繁。导出结果到 CSV 也很重要因为论文复现的过程记录和调参过程都依赖这些导出数据。建议每轮实验都在文件名里包含参数信息比如result_r0.1_rho1.0_scen50.csv避免后面做对比分析时找不到参数来源。7. 复现之后的思考与扩展方向这套框架跑通之后我最大的体会是DRO 并不是一个高不可攀的数学玩具它其实有着很清晰的工程逻辑——用历史数据构造一个“分布的不确定范围”然后在这个范围内做最坏的打算同时通过分布的结构避免传统鲁棒优化过度保守。代码本身就是最好的学习说明书公式里的每个符号在代码里都有对应物。如果你想在这个框架上继续扩展下面几个方向是我个人觉得很值得尝试的把模糊集从 Wasserstein 改成 KL 散度或者矩约束比较不同模糊集对调度结果的保守性差异这是很多论文最喜欢做的事在目标函数中加入碳排放约束或需求响应机制把单目标优化扩展成多目标贴近新型电力系统的实际需求把两层算法从 CCGADMM 换成另一种分布式求解方式比如基于共识的增广拉格朗日算法测试不同场景下的性能差异。最后再分享一个小技巧我在跑这个项目时用过最简单也最有效的调参方式是把模糊集半径先设成 0跑一遍确定性优化再把半径设成非常大的值跑一遍纯鲁棒优化。这两条就可以作为 DRO 结果的理论上下界当你拿到任何一组新参数时先看它是否落在这两条线之间如果偏离了很大绝对不是算法的问题而是参数标定错了。代码复现的价值从来不是跑出一个和论文一模一样的数字——那是魔术不是工程。真正有价值的是你在复现过程中建立起来的模型直觉和算法排错能力它们才是以后面对任何一篇新论文都能快速上手的基础。
返回列表