两阶段鲁棒优化与CCG/Benders算法工程实现
简介本资源是一套面向运筹优化研究者与工程实践者的两阶段鲁棒优化MATLAB实现方案聚焦不确定环境下的决策建模与高效求解特别适用于能源调度、供应链规划、电力系统等含强不确定性场景。压缩包共5个文件含3份核心PDF文献涵盖CCG与Benders算法原理对比、理论推导及应用综述和2个可直接运行的MATLAB代码文件基于YALMIP框架实现CCG列生成与Benders分解全流程总大小1.44MB结构精炼、即开即用。已有2284人学习下载体现了该方法在学术复现与工业建模中的广泛需求。读者可获得从论文理论到代码落地的完整闭环不仅包含原论文算法逻辑的忠实复现还提供模块化函数封装、关键参数注释、收敛性验证机制及扩展接口说明便于快速适配自定义约束与不确定性集显著降低两阶段鲁棒优化的入门与二次开发门槛。1. 两阶段鲁棒优化不是“先算再扛”而是让模型在不确定中主动谈判你手头有一份电力调度计划但风电出力可能比预测高20%或低30%负荷波动范围达±15%设备故障概率虽小却真实存在。此时若用确定性模型拍板结果往往在实际运行中频繁越限、反复调整、成本飙升——这不是模型不准是它根本没被允许“考虑不确定性”。两阶段鲁棒优化Two-Stage Robust Optimization, TSRO正是为这类场景而生它把决策拆成“现在必须定”的第一阶段如机组启停、变压器档位和“等不确定因素揭晓后再响应”的第二阶段如实时出力再分配、备用调用。关键在于它不依赖概率分布只用一个紧凑的不确定性集合如多面体、椭球或预算不确定集刻画最坏但合理的情形并保证无论这个集合里哪个场景发生第二阶段总能找到可行且经济的应对策略。标题中提到的CCGColumn-and-Constraint Generation和Benders分解不是并列选项而是TSRO求解的两种主流路径——前者从主问题出发迭代添加“最坏场景”和对应“最优响应”后者则把第二阶段对偶问题嵌入主问题通过Benders割平面逼近最优解。本文聚焦可复现、可扩展的工程实现所有代码基于PythonPyomoGLPK/CPLEX不依赖特定商业求解器封装参数结构清晰模块边界明确你改一个不确定集定义就能跑通新案例。2. CCG算法不是黑箱循环而是主问题与子问题的攻防博弈CCG算法的核心思想非常直观我们先猜一个“最温和”的不确定性场景解出对应的第一阶段决策然后问——在这个决策下最坏的不确定场景是什么它会导致第二阶段不可行还是成本爆炸如果存在这样的“攻击性场景”我们就把它加进主问题同时把该场景下最优的第二阶段响应即“防御策略”也作为新变量加入。这个过程不断重复直到找不到能显著恶化目标的场景为止。整个流程由主问题Master Problem, MP和子问题Subproblem, SP协同驱动二者角色分明、接口清晰。2.1 主问题构建带历史场景的稳健决策骨架主问题的目标是找到一组第一阶段变量 (x)使得在已知的所有“最坏场景”( \xi^k )(k1,\dots,K)下第二阶段都能给出可行且低成本的响应 (y^k)。其数学形式为[ \begin{aligned} \min_{x, y^1,\dots,y^K} \quad c^\top x \max_{k1,\dots,K} \left( d^\top y^k \right) \ \text{s.t.} \quad A x \leq b \ T^k x W y^k \leq h^k, \quad \forall k 1,\dots,K \ x \in \mathcal{X}, ; y^k \in \mathcal{Y} \end{aligned} ]其中 (T^k, h^k) 是第 (k) 个场景 (\xi^k) 对应的系数矩阵和右端项。注意目标函数中的 (\max) 项——它迫使主问题不仅要满足所有约束还要最小化最差场景下的第二阶段成本。在Pyomo中我们不直接写 (\max)而是引入辅助变量 (\eta) 并添加约束 (\eta \geq d^\top y^k)将目标改为 (\min, c^\top x \eta)。这样主问题就是一个标准的混合整数线性规划MILP可被GLPK、CBC或CPLEX直接求解。# pyomo_ccg_master.py: 主问题建模简化核心 from pyomo.environ import * def build_master_problem(scenarios): model ConcreteModel() # 第一阶段变量例如机组启停状态 model.x Var(range(n_x), domainBinary) # 每个已知场景k对应的第二阶段变量y^k model.y Var(range(len(scenarios)), range(n_y), domainReals) model.eta Var() # 辅助变量代表最坏场景成本 # 第一阶段约束Ax b def first_stage_constr_rule(model, i): return sum(A[i, j] * model.x[j] for j in range(n_x)) b[i] model.first_stage_constr Constraint(range(len(b)), rulefirst_stage_constr_rule) # 场景k下的第二阶段可行性约束T^k x W y^k h^k def second_stage_constr_rule(model, k, i): lhs sum(T_list[k][i, j] * model.x[j] for j in range(n_x)) lhs sum(W[i, l] * model.y[k, l] for l in range(n_y)) return lhs h_list[k][i] model.second_stage_constr Constraint(range(len(scenarios)), range(len(h_list[0])), rulesecond_stage_constr_rule) # 确保 eta d^T y^k 对所有k成立 def eta_bound_rule(model, k): return model.eta sum(d[l] * model.y[k, l] for l in range(n_y)) model.eta_bound Constraint(range(len(scenarios)), ruleeta_bound_rule) # 目标最小化第一阶段成本 最坏第二阶段成本 model.obj Objective(exprsum(c[j] * model.x[j] for j in range(n_x)) model.eta, senseminimize) return model提示T_list[k]和h_list[k]是预计算好的场景k对应的系数矩阵和右端向量。它们通常由不确定性参数 (\xi) 的具体取值代入原始模型生成。不要在主问题内实时计算这些矩阵——那会破坏MP的线性结构导致求解器无法识别。2.2 子问题寻找当前x下最致命的不确定性场景子问题的任务是给定当前主问题解出的第一阶段决策 (\bar{x})在全部可能的不确定性集合 (\mathcal{U}) 中找出使第二阶段问题最“难解”的那个 (\xi^*)。这本质上是一个极大极小问题max-min最大化第二阶段的最小成本或最小化可行性裕度。对于经典预算不确定集 (\mathcal{U} {\xi \mid |\xi - \hat{\xi}|_1 \leq \Gamma, , \xi_L \leq \xi \leq \xi_U})子问题可精确线性化为一个线性规划LP。# pyomo_ccg_subproblem.py: 子问题建模以预算不确定集为例 def build_subproblem(x_bar, xi_hat, Gamma, xi_L, xi_U, d, W_inv, h_base): x_bar: 当前第一阶段解NumPy array W_inv: 第二阶段约束矩阵W的伪逆或预处理结构用于快速构建T(xi) h_base: 不确定性为0时的基准右端项 model ConcreteModel() # 不确定性变量 xi model.xi Var(range(len(xi_hat)), boundslambda m, i: (xi_L[i], xi_U[i])) # 预算约束sum |xi_i - xi_hat_i| Gamma model.budget_constr Constraint( exprsum(abs(model.xi[i] - xi_hat[i]) for i in range(len(xi_hat))) Gamma ) # 构造当前xi下的第二阶段对偶问题极大化问题 # 原第二阶段min d^T y s.t. T(xi)x_bar W y h(xi) # 其对偶为max pi^T (h(xi) - T(xi)x_bar) s.t. pi^T W d^T, pi 0 # 我们最大化这个对偶目标即寻找最坏xi model.pi Var(range(len(h_base)), domainNonNegativeReals) # 对偶可行性pi^T W d^T def dual_feas_rule(model, j): return sum(model.pi[i] * W[i, j] for i in range(len(h_base))) d[j] model.dual_feas Constraint(range(len(d)), ruledual_feas_rule) # 目标max pi^T (h(xi) - T(xi)x_bar) # h(xi) h_base H * xi 线性扰动 # T(xi)x_bar T_base * x_bar sum_i (T_delta_i * x_bar) * xi_i linear_term sum(model.pi[i] * (h_base[i] - sum(T_base[i,j] * x_bar[j] for j in range(len(x_bar))))) xi_coeff [] for i in range(len(h_base)): coeff_i sum(model.pi[i] * (H[i,k] - sum(T_delta[k][i,j] * x_bar[j] for j in range(len(x_bar))))) xi_coeff.append(coeff_i) model.obj Objective(exprlinear_term sum(xi_coeff[i] * model.xi[i] for i in range(len(xi_hat))), sensemaximize) return model注意子问题的目标函数推导依赖于第二阶段模型关于 (\xi) 的线性结构假设即 (T(\xi) T^0 \sum_i T^i \xi_i), (h(\xi) h^0 H\xi)。这是绝大多数工程应用如OPF、UC的默认设定。若你的模型含非线性不确定项子问题将变为非凸需改用全局优化器如BARON或近似线性化。2.3 CCG主循环收敛判定与场景管理的关键细节CCG的终止条件不是“迭代次数到100”而是“子问题返回的最坏场景成本提升低于阈值”。具体来说设第 (k) 轮主问题最优值为 (z^{MP}_k)子问题在 (\bar{x}_k) 下返回的最坏第二阶段成本为 (z^{SP}_k)则定义对偶间隙为 [ \text{Gap}_k \frac{z^{SP}_k - z^{MP}_k}{|z^{MP}_k| 1e-8} ] 当 (\text{Gap}_k \epsilon)如 (1e-4)时算法收敛。实践中还需设置最大迭代次数如50和子问题无界/不可行的异常处理分支。# ccg_main_loop.py: 完整主循环含健壮性检查 from pyomo.opt import SolverFactory def run_ccg(max_iter50, eps1e-4): # 初始化至少包含一个“中心”场景 scenarios [xi_hat] # 初始场景列表 solver SolverFactory(glpk) # 或 cplex, gurobi for iter_num in range(1, max_iter 1): print(f--- CCG Iteration {iter_num} ---) # Step 1: 求解主问题 mp_model build_master_problem(scenarios) results_mp solver.solve(mp_model, teeFalse) if results_mp.solver.status ! SolverStatus.ok: raise RuntimeError(fMaster problem infeasible at iteration {iter_num}) x_bar np.array([value(mp_model.x[j]) for j in range(n_x)]) z_mp value(mp_model.obj) # Step 2: 求解子问题获取最坏场景xi_star和对应成本z_sp sp_model build_subproblem(x_bar, xi_hat, Gamma, xi_L, xi_U, d, W_inv, h_base) results_sp solver.solve(sp_model, teeFalse) if results_sp.solver.status ! SolverStatus.ok: # 子问题无界意味着当前x_bar下第二阶段恒可行z_sp 0 z_sp 0.0 xi_star xi_hat else: z_sp value(sp_model.obj) xi_star np.array([value(sp_model.xi[i]) for i in range(len(xi_hat))]) # Step 3: 计算对偶间隙 gap abs(z_sp - z_mp) / (abs(z_mp) 1e-10) print(fMP obj: {z_mp:.6f}, SP worst-cost: {z_sp:.6f}, Gap: {gap:.2e}) # Step 4: 收敛判断 if gap eps: print(fCCG converged at iteration {iter_num} with gap {gap:.2e}) return x_bar, scenarios # Step 5: 若未收敛将新场景xi_star加入列表 # 关键需检查xi_star是否已存在于scenarios中避免重复添加 is_new True for existing_xi in scenarios: if np.allclose(xi_star, existing_xi, atol1e-5): is_new False break if is_new: scenarios.append(xi_star.copy()) print(fAdded new scenario: {xi_star.round(4)}) else: print(New scenario already exists. Breaking to avoid cycling.) break raise RuntimeError(fCCG failed to converge within {max_iter} iterations.)提示scenarios列表的管理是可扩展性的基石。本例中我们仅存储 (\xi) 向量但实际项目中每个scenario应是一个字典包含xi,T_matrix,h_vector,name等字段。这样在后续做敏感性分析或可视化时可直接索引。3. Benders分解用对偶信息切掉不可行的第一阶段决策Benders分解与CCG走的是不同路径它不显式枚举场景而是将第二阶段问题视为一个关于第一阶段变量 (x) 的函数 (Q(x))并利用其凸性对连续y或分段线性性对整数y用一系列线性不等式Benders割来近似 (Q(x))。对于两阶段鲁棒优化Benders方法通常作用于第二阶段的对偶问题因此也被称为“对偶Benders”。其优势在于当不确定性维度高、场景数量爆炸时Benders可能比CCG更节省内存劣势是初始割平面质量差前期收敛慢且对第二阶段含整数变量的情况处理更复杂。3.1 Benders主问题用割平面逐步逼近Q(x)Benders主问题MP的形式为 [ \begin{aligned} \min_{x, \theta} \quad c^\top x \theta \ \text{s.t.} \quad A x \leq b \ \theta \geq \alpha^k \beta^k^\top x, \quad \forall k 1,\dots,K \ x \in \mathcal{X}, ; \theta \in \mathbb{R} \end{aligned} ] 其中 (\theta) 是对 (Q(x)) 的上界估计而 (\theta \geq \alpha^k \beta^k^\top x) 是第 (k) 个Benders割。这里的 (\alpha^k, \beta^k) 来源于子问题在某个 (x^k) 处的对偶解。对鲁棒优化子问题本身是一个极大化问题找最坏 (\xi)因此其对偶即Benders的子问题是一个极小化问题其最优解直接给出割的系数。3.2 鲁棒Benders子问题一个带内层max的LP给定候选解 (\bar{x})Benders子问题SP为 [ Q(\bar{x}) \max_{\xi \in \mathcal{U}} ; \min_{y} \left{ d^\top y ;\middle|; T(\xi)\bar{x} W y \leq h(\xi), ; y \in \mathcal{Y} \right} ] 这是一个max-min双层问题。将其内层min对偶化得到单层max问题 [ Q(\bar{x}) \max_{\xi \in \mathcal{U}, , \pi \geq 0} \left{ \pi^\top \big(h(\xi) - T(\xi)\bar{x}\big) ;\middle|; \pi^\top W \leq d^\top \right} ] 这正是我们在CCG子问题中求解的形式因此Benders的SP与CCG的SP在数学上完全一致。区别仅在于CCG将SP的最优解 (\xi^) 加入MP作为新场景而Benders则提取SP的对偶解 (\pi^)并构造一个Benders割 [ \theta \geq (\pi^)^\top \big(h(\xi^) - T(\xi^) x\big) ] 由于 (\pi^) 和 (\xi^*) 都是SP的输出该割是关于 (x) 的线性函数。# benders_subproblem.py: 与CCG子问题完全相同但输出用于构造割 def solve_benders_subproblem(x_bar, ...): # ... 同CCG子问题建模 ... results solver.solve(model, teeFalse) pi_star np.array([value(model.pi[i]) for i in range(len(h_base))]) xi_star np.array([value(model.xi[i]) for i in range(len(xi_hat))]) # 计算割的常数项 alpha pi_star^T (h(xi_star) - T(xi_star) x_bar) h_xi h_base H xi_star T_xi_xbar T_base x_bar for i in range(len(xi_star)): T_xi_xbar T_delta[i] x_bar * xi_star[i] alpha pi_star (h_xi - T_xi_xbar) # 计算割的斜率 beta - pi_star^T * (dT/dx) 此处dT/dx即T_delta的线性组合 beta np.zeros(len(x_bar)) for j in range(len(x_bar)): for i in range(len(xi_star)): beta[j] - pi_star T_delta[i][:, j] * xi_star[i] return alpha, beta, xi_star # benders_main.py: Benders主循环 def run_benders(max_iter50, eps1e-4): # 初始化MP只有第一阶段约束 mp_model build_benders_master() solver SolverFactory(glpk) for iter_num in range(1, max_iter 1): # 求解MP获取x_bar, theta_bar results_mp solver.solve(mp_model, teeFalse) x_bar np.array([value(mp_model.x[j]) for j in range(n_x)]) theta_bar value(mp_model.theta) # 求解SP获取alpha, beta, xi_star alpha, beta, xi_star solve_benders_subproblem(x_bar, ...) q_val alpha beta x_bar # Q(x_bar)的估计值 # 计算间隙 gap abs(q_val - theta_bar) / (abs(theta_bar) 1e-10) if gap eps: return x_bar # 添加Benders割theta alpha beta^T x add_benders_cut(mp_model, alpha, beta)注意Benders割的推导严格依赖于第二阶段对偶问题的强对偶性。如果第二阶段含整数变量如y为0-1强对偶不成立此时需使用“L-shaped method”或“integer L-shaped”其割包含额外的整数可行性约束实现更复杂。本文默认第二阶段y为连续变量这是电力系统经济调度等场景的常见设定。4. 可扩展改编从论文公式到生产级代码的三大改造点原论文的CCG/Benders代码往往是为单一案例定制的MATLAB脚本变量名如x1,u2,objval缺乏抽象和复用。要让它真正“可扩展”必须完成三重改造结构解耦、参数外置、场景即服务。这不仅是代码风格问题更是决定你能否在一周内把风电鲁棒调度模型无缝迁移到氢能储运鲁棒规划上的关键。4.1 结构解耦用类封装场景生成与模型构建逻辑将不确定性集合 (\mathcal{U}) 的定义、场景生成逻辑、以及T/h矩阵的组装全部封装进一个UncertaintySet类。它不关心优化求解只负责“翻译”输入一个 (\xi) 向量输出对应的T_matrix和h_vector。这样当你从预算集切换到椭球集时只需继承该类并重写get_T_h()方法主算法循环一行代码都不用动。# uncertainty_set.py class UncertaintySet: def __init__(self, xi_hat, xi_L, xi_U): self.xi_hat xi_hat self.xi_L xi_L self.xi_U xi_U def is_in_set(self, xi): 判断xi是否属于该集合用于验证 raise NotImplementedError def get_T_h(self, xi, x_baseNone): 给定xi返回T(xi)和h(xi)。x_base可选用于非线性项 raise NotImplementedError class BudgetUncertaintySet(UncertaintySet): def __init__(self, xi_hat, xi_L, xi_U, Gamma): super().__init__(xi_hat, xi_L, xi_U) self.Gamma Gamma def is_in_set(self, xi): return np.sum(np.abs(xi - self.xi_hat)) self.Gamma def get_T_h(self, xi, x_baseNone): # 线性扰动T(xi) T_base sum_i T_delta[i] * xi[i] T self.T_base.copy() for i in range(len(xi)): T self.T_delta[i] * xi[i] h self.h_base self.H xi return T, h # 在主程序中 uncertainty BudgetUncertaintySet(xi_hat, xi_L, xi_U, Gamma3.0) # 后续所有T, h的获取都通过 uncertainty.get_T_h(xi)4.2 参数外置用YAML配置文件管理所有可调参数硬编码的Gamma2.5,n_x12,solvercplex是可扩展性最大的敌人。创建config.yaml将所有业务参数、求解器参数、算法参数分离# config.yaml problem: name: wind_power_dispatch n_x: 15 n_y: 20 c: [12.5, 15.0, 18.2, ...] # 第一阶段成本向量 uncertainty: type: budget xi_hat: [0.0, 0.0, 0.0] xi_L: [-0.3, -0.2, -0.1] xi_U: [0.4, 0.3, 0.2] Gamma: 2.8 algorithm: method: ccg # or benders max_iter: 40 tolerance: 1e-4 initial_scenarios: [center, worst_case] solver: name: gurobi options: MIPGap: 1e-4 TimeLimit: 3600加载配置后主函数只需run_algorithm(config)无需任何参数硬编码。团队成员修改Gamma或换求解器只需改YAML不碰Python。4.3 场景即服务用函数式接口支持动态场景注入CCG循环中scenarios列表是静态的。但在实际部署中你可能需要从数据库实时拉取最新气象预报作为新场景根据在线学习模型动态生成对抗性场景将CCG与蒙特卡洛模拟结合用统计显著性检验场景有效性。为此将场景管理升级为一个ScenarioManager类它提供add_scenario(xi)和get_all_scenarios()接口并内置去重、归一化、缓存功能。更重要的是它支持“延迟加载”——get_all_scenarios()返回的不是numpy数组而是一个生成器generator每次yield一个(xi, T, h)元组。这样即使有10万场景内存占用也恒定。# scenario_manager.py class ScenarioManager: def __init__(self): self._scenarios [] # 存储(xi, name)元组 self._cache {} # 缓存(xi - (T, h))避免重复计算 def add_scenario(self, xi, nameNone): if not self._is_duplicate(xi): self._scenarios.append((xi, name or fscen_{len(self._scenarios)})) def get_all_scenarios(self, uncertainty_set): 返回生成器按需计算T/h for xi, name in self._scenarios: if tuple(xi) not in self._cache: self._cache[tuple(xi)] uncertainty_set.get_T_h(xi) yield xi, name, self._cache[tuple(xi)] def _is_duplicate(self, xi, tol1e-5): return any(np.allclose(xi, old_xi, atoltol) for old_xi, _ in self._scenarios) # 使用方式 manager ScenarioManager() manager.add_scenario(xi_hat) # 中心场景 manager.add_scenario(worst_case_from_db()) # 从DB加载 for xi, name, (T, h) in manager.get_all_scenarios(uncertainty): # 构建MP约束无需预先加载全部T/h到内存 add_second_stage_constraint(mp_model, T, h, name)提示ScenarioManager的设计直接支撑了“可扩展改编”的终极形态——你可以编写WeatherForecastScenarioSource或AdversarialAttackScenarioSource它们都实现同一个get_next_scenario()接口主算法循环完全 unaware 具体来源。这才是真正的面向接口编程。5. 验证与调试用三类测试确保你的鲁棒模型不“纸糊”一个鲁棒优化模型跑出数字不等于它真的鲁棒。必须通过三类测试交叉验证可行性测试、最坏场景压力测试、与确定性模型的对比测试。每类测试都应自动化集成到CI/CD流水线中。5.1 可行性测试穷举验证所有已知场景下的第二阶段这是最基础的守门员测试。取CCG最终收敛得到的 (x^) 和所有已加入的 (K) 个场景 (\xi^k)对每个 (k)独立求解第二阶段问题 [ \min_y ; d^\top y \quad \text{s.t.} ; T(\xi^k) x^ W y \leq h(\xi^k), ; y \in \mathcal{Y} ] 若任一 (k) 导致第二阶段不可行infeasible说明CCG循环有bug或子问题建模错误如遗漏了某个约束。此测试应100%通过。# test_feasibility.py def test_feasibility(x_star, scenarios, uncertainty): solver SolverFactory(glpk) for k, (xi, name) in enumerate(scenarios): T, h uncertainty.get_T_h(xi) # 构建第二阶段模型 model ConcreteModel() model.y Var(range(n_y), domainReals) def constr_rule(model, i): return sum(T[i, j] * x_star[j] for j in range(len(x_star))) \ sum(W[i, l] * model.y[l] for l in range(n_y)) h[i] model.constr Constraint(range(len(h)), ruleconstr_rule) model.obj Objective(exprsum(d[l] * model.y[l] for l in range(n_y)), senseminimize) results solver.solve(model, teeFalse) if results.solver.status ! SolverStatus.ok: raise AssertionError(fFeasibility test failed for scenario {name} (k{k})) # 运行 test_feasibility(x_opt, final_scenarios, uncertainty)5.2 最坏场景压力测试用随机采样挑战算法的“最坏”宣称CCG声称找到了“最坏场景”但它的子问题求解器如GLPK可能因数值误差返回次优解。为验证我们进行压力测试在不确定性集合 (\mathcal{U}) 内随机采样1000个 (\xi)对每个 (\xi)计算第二阶段在 (x^) 下的最小成本 (Q(x^, \xi))。然后检查CCG报告的“最坏成本” (z^{SP}) 是否确实是这1000个值中的最大值允许1e-3数值误差。如果不是说明子问题建模有缺陷或求解器精度不足。# test_worst_case.py def test_worst_case(x_star, uncertainty, n_samples1000): # 在U内随机采样 samples [] for _ in range(n_samples): xi np.random.uniform(uncertainty.xi_L, uncertainty.xi_U) if uncertainty.is_in_set(xi): # 预筛选提高效率 samples.append(xi) # 计算每个样本下的Q(x*, xi) q_values [] solver SolverFactory(glpk) for xi in samples: T, h uncertainty.get_T_h(xi) # 构建并求解第二阶段 model build_second_stage_model(x_star, T, h, d, W) results solver.solve(model, teeFalse) if results.solver.status SolverStatus.ok: q_values.append(value(model.obj)) else: q_values.append(float(inf)) # 不可行视为无穷大成本 max_sampled max(q_values) # CCG报告的最坏成本来自最后一次SP求解 z_sp_reported ... # 从CCG日志或返回值中获取 assert abs(max_sampled - z_sp_reported) 1e-3, \ fWorst-case mismatch: sampled {max_sampled:.6f} vs reported {z_sp_reported:.6f} test_worst_case(x_opt, uncertainty)5.3 确定性对比测试量化“鲁棒溢价”的合理性最后必须回答业务方的灵魂拷问“你这个鲁棒模型比我们一直用的确定性模型贵多少值不值” 运行确定性模型即令 (\xi \hat{\xi})得到最优解 (x^{det}) 和成本 (z^{det})。再将 (x^{det}) 代入鲁棒模型的第二阶段计算其在CCG找到的最坏场景 (\xi^) 下的成本 (Q(x^{det}, \xi^))。鲁棒溢价为 [ \text{Premium} \frac{z^{robust} - z^{det}}{z^{det}} \times 100% ] 若溢价超过20%而业务无法接受说明不确定性集合 (\mathcal{U}) 设得太宽需与领域专家重新校准 (\Gamma) 或 (\xi_U/\xi_L)。这个测试不是为了否定鲁棒模型而是为了将其参数调校到业务可接受的平衡点。# test_deterministic_comparison.py def compare_with_deterministic(x_robust, x_det, scenarios, uncertainty): # 确定性模型成本 T_det, h_det uncertainty.get_T_h(uncertainty.xi_hat) z_det solve_second_stage(x_det, T_det, h_det, d, W) # 鲁棒模型成本MP目标值 z_robust ... # 从CCG返回值获取 # 计算确定性解在最坏场景下的成本 xi_worst scenarios[-1][0] # 最后一个场景通常是CCG找到的最坏场景 T_worst, h_worst uncertainty.get_T_h(xi_worst) z_det_worst solve_second_stage(x_det, T_worst, h_worst, d, W) premium (z_robust - z_det) / (z_det 1e-10) * 100 vulnerability (z_det_worst - z_det) / (z_det 1e-10) * 100 print(fDeterministic cost: {z_det:.4f}) print(fRobust cost: {z_robust:.4f} (Premium: {premium:.2f}%)) print(fDeterministic solution under worst case: {z_det_worst:.4f} (Vulnerability: {vulnerability:.2f}%)) # 业务规则溢价应 15%脆弱性应 30%否则鲁棒性无意义 assert premium 15.0, fRobust premium {premium:.2f}% exceeds business threshold. assert vulnerability 30.0, fDeterministic solution too robust, uncertainty set may be too narrow. compare_with_deterministic(x_opt, x_det_solution, final_scenarios, uncertainty)注意vulnerability脆弱性指标至关重要。如果z_det_worst只比z_det高1%说明你设定的不确定性范围太保守鲁棒优化没有发挥价值如果高50%则说明确定性方案风险极高鲁棒溢价15%就是合理的保险费。这个数字才是你向业务部门汇报时最有说服力的一页PPT。本文还有配套的精品资源点击获取