1. 问题拆解:数据中心微网规划到底在优化什么
做这个课题之前,我一直有个直觉——数据中心微网和普通园区微网最大的区别不在拓扑上,而在“负荷”本身。普通微网规划,负荷基本是刚性的,你只能被动去匹配。但数据中心不一样,它的IT负载、UPS储能、制冷系统,甚至算力调度都能参与调节,这就把传统的“源-荷”规划问题变成了“源-网-荷-储-算”的耦合优化问题。所以论文标题里“考虑灵活性”这五个字,其实是整篇的题眼。
我们这套两阶段鲁棒规划,本质上回答两个层面的问题:
- 第一阶段(投资决策):在光伏、储能、柴油发电机、燃气轮机这些候选设备里,选哪些、装多大容量,让总投资成本最小。这个决策是“现在就要拍板”的,建完就不能改了。
- 第二阶段(运行决策):在设备装完之后,面对每天波动的负荷、光伏出力和电价,怎么调度这些设备,让日运行成本最小。这个决策是“每天滚动做的”,随不确定性的实现而调整。
两阶段之间通过投资容量耦合:你第一阶段买的储能容量越大,第二阶段调度的灵活空间就越大,运行成本就越低,但投资成本也越高。这个权衡关系,单阶段确定性优化是算不清的,必须用两阶段框架。
为什么偏偏要加“鲁棒”?因为数据中心的供电可靠性要求太高了,四个九、五个九是常态。你如果按确定性场景优化,万一光伏出力没预测得那么高、或者IT负载突发高峰,就可能出现切负荷。鲁棒优化的思路不赌某一个场景,而是构建一个不确定集,保证集合内最恶劣的情形下系统也能安全运行。代价是结果偏保守,但数据中心这类负荷,保守一点完全合理。
另外,论文标题里的“EI复现”指的是复现已发表期刊论文的方法。这里我特别想提醒一句:EI论文的复现,核心难点从来不是读懂公式,而是把数学建模转成可求解的数学规划,再在Matlab里落地。这个从公式到代码的鸿沟,远比想象中大。文章里我会把整套建模和代码实现思路拆开讲透。
2. 两阶段优化框架的建模思路
2.1 第一阶段:投资决策模型
第一阶段决策变量是设备选型与容量配置,常见候选设备包括:
- 光伏(PV):单位容量投资成本约5000-8000元/kW,使用寿命20年上下;
- 储能(BESS):单位容量投资成本约1500-2500元/kWh,功率成本约2000-3000元/kW;
- 柴油发电机(DG):单位容量投资成本约1500-3000元/kW,运维成本高,碳排放高;
- 燃气轮机(MT):单位容量投资成本约4000-7000元/kW,效率高但需要天然气供应。
复现时,我习惯用x_pv, x_bess_e, x_bess_p, x_dg, x_mt这些0-1变量表示“是否建设”,用C_pv, C_bess, C_dg, C_mt等连续变量表示“建设容量”。投资成本目标函数写成:
inv_cost = sum(C_inv_pv .* C_pv) + sum(C_inv_bess_e .* C_bess_e + C_inv_bess_p .* C_bess_p) + ...注意储能要分能量(kWh)和功率(kW)两个维度分别建容量,因为成本构成不同,运行约束也是分开的。这个细节不处理好,后面的充放电约束很容易写错。
第一阶段还含二进制变量,所以整问题是MILP(混合整数线性规划)。我用YALMIP定义变量时,最常踩的坑是指数维度不匹配,电池的充放电非负变量维度写成(T,1)但容量变量写成(1,1),导致约束传不进去。建议统一用repmat扩展成场景数×时段数的矩阵再说。
确定性等价形式下,第一阶段目标函数是:
投资成本 + 所有场景下第二阶段运行成本的期望
第二阶段换成鲁棒,就不是期望了,而是“所有场景里最恶劣的那个场景下的运行成本”。这时候目标函数变成:
投资成本 + max_{u∈U} min_{y∈Ω(x,u)} 运行成本
这个max-min结构,就是两阶段鲁棒的核心。
2.2 第二阶段:运行决策模型
第二阶段运行决策变量包括:
| 变量 | 含义 | 常见边界 |
|---|---|---|
| P_pv(t) | 光伏实际出力 | 0 ≤ P_pv ≤ P_pv_pred |
| P_ch(t), P_dis(t) | 储能充、放电功率 | 0 ≤ P ≤ P_max,且不能同时充放 |
| SOC(t) | 储能荷电状态 | 0.1 ≤ SOC ≤ 0.9 |
| P_dg(t), P_mt(t) | 柴油机/燃气轮机出力 | 0 ≤ P ≤ P_rated |
| P_buy(t), P_sell(t) | 与上级电网购/售电 | P_buy ≥ 0,P_sell ≥ 0 |
| P_it(t) | IT负载实际消耗(可调节) | P_it_min ≤ P_it ≤ P_it_max |
功率平衡约束(节点k):
P_pv(k,t) + P_dis(k,t) + P_dg(k,t) + P_mt(k,t) + P_buy(k,t) = P_it(k,t) + P_ch(k,t) + P_sell(k,t) + P_cool(k,t)
制冷负载P_cool和IT负载有耦合关系——服务器功率越高,发热越大,制冷耗电越多。简化建模里我常写P_cool = α·P_it,α取0.3~0.5。这个耦合关系就是“灵活性”的一部分,也是数据中心区别于普通园区的地方。
储能充放电约束是第二阶段最容易出错的地方:
SOC(t+1) = SOC(t) + (eta_ch * P_ch(t) - P_dis(t) / eta_dis) * dt / E_rated; P_ch(t) + P_dis(t) <= P_rate_max; % 不能同时充放电(或用0-1约束)这里dt的单位要和容量单位一致。如果用小时,dt=1;如果用15分钟间隔,dt=0.25。我见过很多论文复现跑出SOC漂移,十有八九是dt和容量单位没对齐。
2.3 为什么不能直接求解max-min问题
max-min结构不能直接丢给求解器,原因很简单:max和min分别对应不同玩家的决策,目标函数在两者之间是鞍点关系,不满足单一优化问题的KKT条件。你得先想办法把它转成单层问题。
常见的三种转化路线:
- 对偶转化:把min问题取对偶,变成max-max结构,再把外层max合并。适用于第二阶段是LP(线性规划)的情形。
- KKT条件:把内层min问题的KKT条件写出来,加互补松弛约束,变成单层MPEC(数学规划带均衡约束)。但互补松弛含非线性项,处理麻烦。
- C&CG(列与约束生成)算法:把min问题按场景拆开,通过迭代不断添加场景约束和对应变量,逐步逼近最恶劣场景。这是目前最主流的方法,也是我复现时首选的路线。
C&CG的思路用大白话说:先假设一个最恶劣场景,求解两阶段问题,得到一个投资方案和运行方案;然后固定这个投资方案,去搜一个让系统运行成本更高的新场景;如果新场景确实更恶劣,就把这个场景对应的约束加进去重新求解,否则停止。这样反复迭代,相当于把“无穷多场景”的安全约束,用有限次迭代一点一点压进模型里。
3. 灵活性的建模细节:数据中心微网的独家变量
3.1 IT负载的时移特性与算力调度
数据中心最值钱的灵活性来自IT负载本身。一个大型数据中心内可能有成千上万个虚拟机、容器或批处理任务,它们不是必须在某个具体时刻运行——比如离线训练任务、数据备份、日志处理,晚一两个小时跑完完全没问题。
建模上,我通常把IT负载分成两类:
- 刚性负载:必须实时供电的在线业务,比如Web请求、数据库查询,P_it_fixed;
- 柔性负载:可以时移或调度的批处理任务,P_it_flex。
柔性负载的灵活性约束长这样:
% 某个时间窗内的总能耗必须满足任务需求 sum(P_it_flex(t_start:t_end)) >= E_task; % 每个时刻的功率有上限 P_it_flex(t) <= P_it_flex_max(t);这个模型的价值在于,当电价高或光伏出力低时,系统可以压低柔性负载,把用电平移到光伏大发或电价低谷时段。这比单纯配储能的成本要低得多,因为算力调度本身不产生额外投资。
复现时,如果你想简化,可以不用建任务队列这么细,直接给一个可调比例系数:P_it(t) = P_it_fixed(t) + delta_flex(t)·P_it_flex_max,其中delta_flex是一个0-1可调变量,表示柔性负载的参与率。这样既能体现灵活性,又不会让模型复杂到难以收敛。
另外提醒一下:服务器功耗不要按额定功率算。实测中服务器负载率在10%到100%之间波动,功耗非线性增长。建模里如果追求精度,用P_server = P_idle + (P_max - P_idle)·u,其中u是CPU利用率。但考虑到MILP求解规模,我在复现时直接用线性近似就足够了——目标函数的误差通常小于3%。
3.2 储能与UPS的双重身份
数据中心本来就有UPS(不间断电源),正常运行时就是给服务器“保驾护航”,但在微网框架下,UPS完全可以兼职当储能用。
UPS通常有两类:
- 在线式双变换UPS:正常时AC-DC-AC双重变换,效率85%~95%;
- 后备式UPS:正常时旁路直供,断电时才切到电池。
从规划角度看,UPS电池如果能参与日常调度,那就相当于免费获得了一部分储能容量。但有两个坑:
- UPS电池循环寿命有限,铅酸电池在1000~1500次循环左右,锂电好一些,但频繁充放会加速衰减。如果规划模型不考虑寿命折损,会高估UPS参与调度的收益。
- UPS的充电策略通常是浮充,设计寿命7~10年按浮充算,改成循环充放后可能3年就报废。所以更严谨的做法是加一个全生命周期成本修正系数,复现时可以把UPS循环成本设成0.5~1.0元/kWh,用于惩罚深度充放。
如果论文里没有明确区分UPS和储能,代码实现时可以合并建模,但需要把效率参数分开设置。UPS放电效率通常比储能略低几个百分点,对5%~8%的效率差异敏感的结果要小心。
3.3 制冷系统与电冷耦合
数据中心的制冷系统能耗通常占IT设备能耗的30%~50%,是仅次于IT设备的第二耗电大户。考虑到灵活性,制冷系统有几个可玩的空间:
- 蓄冷罐:像储能电池一样,低谷电价时段蓄冷,高峰时段释冷,减少电制冷机组启停;
- 温度调宽:机房温度设定点从22°C放宽到27°C,制冷负荷可以压降10%~20%,但对IT设备的可靠性有边际影响;
- 自然冷源:在寒冷地区直接引入室外空气冷却,替代电制冷,减少用电。
复现时如果论文模型包含电冷耦合,我推荐用COP(能效比)建一个简化模型:
P_cool(t) = Q_cool(t) / COP(t); Q_cool_min <= Q_cool(t) <= Q_cool_max;COP和环境温度相关,夏天低冬天高,但简化时取一个典型值3.5~5.0也行。蓄冷罐则可以参照储能建模,只是“电”换成“冷”,SoC换成蓄冷量,充放变量换成蓄冷/释冷功率。
我自己的经验是:除非论文里明确研究制冷灵活性,否则不要一上来就把制冷细节全建模进去,否则第二阶段问题维度爆炸,C&CG迭代一次要算很久。先用固定比例P_cool=α·P_it做基准,再逐步加复杂度。
4. 两阶段鲁棒规划的核心算法:C&CG的Matlab实现
4.1 不确定集怎么定义
两阶段鲁棒里最关键的抽象是“不确定性从哪里来”。数据中心微网的不确定性源主要有三个:
- 光伏出力波动:受天气影响,预测误差可达20%~30%;
- IT负载波动:受业务流量影响,尤其在电商大促、流量突增时段波动明显;
- 电价波动:现货市场电价有时段性,也可能出现极端尖峰。
最简单的处理方式是盒式不确定集:
u ∈ [u_min, u_max]
也就是每个不确定参数都落在预测值加减一个偏差范围内。这个模型简单,但有个问题是它会包含所有参数同时达到最恶劣值的场景,实际几乎不可能发生,导致结果过度保守。
更合理的是预算不确定集(Budget Uncertainty Set),也叫Budgeted Uncertainty Set,它在盒式基础上附加一个总量限制:
sum |u_i - u_i_mean| / delta_i <= Gamma
Gamma是预算参数,控制所有参数同时偏离预测值的总程度。如果Gamma=0,就是确定性模型;Gamma越大,鲁棒性越强,成本越高。复现时要跑Gamma的敏感性分析,画一条“成本-鲁棒性”权衡曲线,这种图在论文里非常有说服力。
那谁来充当“决策者”选择最恶劣场景呢?就是外层max问题。我们不需要人为指定哪个场景最恶劣,而是在不确定集范围内搜索,让系统运行成本最大化。C&CG每轮迭代就是在做这个搜索。
4.2 C&CG算法流程与Matlab伪代码
C&CG迭代求解两阶段鲁棒问题的算法流程如下:
- 初始化:LB = -inf,UB = inf,k = 1,初始不确定场景取预测均值(或某一个名义场景);
- 主问题(MP):在第一阶段投资决策基础上,纳入前k轮识别出的所有恶劣场景的运行约束,求解得到最优投资方案x和总成本。更新下界LB = MP目标值;
- 子问题(SP):固定主问题求出的投资方案x,求解第二阶段运行问题在不确定集下的max-min形式,找出最恶劣场景u_k+1。子问题的最优值加上已求得的投资成本(按对应x重新算投资成本再叠加),更新上界UB;
- 收敛判断:如果(UB-LB)/UB <= epsilon(通常取1e-3),停止;否则k=k+1,把新场景u_k+1对应的运行变量和约束添加到主问题中,回到第2步。
Matlab+YALMIP的框架下,C&CG实现不会太复杂:
在第k轮迭代时,我们固定了不确定变量u的取值(比如光伏出力、IT负载、电价都取当前最恶劣场景的具体值),此时子问题变成:
min_{y} f(y) (线性目标,固定u) s.t. A(x*) + B(y) ≤ C(u_k) + D y ≥ 0
这个子问题直接调用线性规划求解器解出最优值,给出该场景下的最小运行成本。然后我们在不确定集U上搜索新场景u_k+1,让这个最优值最大。实际C&CG实现中,最恶劣场景不一定需要显式搜,有些实现直接把子问题的对偶乘子用来构造最优割平面,能更快收敛。C&CG的优势在于它不断把新场景对应的运行变量和约束直接加进主问题,而不是通过对偶加割平面,因此主问题是完整MILP,求解器能利用它的结构。
Matlab实现中,主问题和子问题要用不同的优化变量对象。我们通常是复制一套子问题变量,每次迭代把新场景的约束加到主问题里,再把部分变量“绑定”到上一轮的解上。这就特别考验变量索引管理:如果从第1轮开始就规划好统一的命名规则,比如y{k}{t,1}代表第k个场景t时段的变量,后面加场景就是加一行索引,不会乱。
4.3 求解器选型与参数调优
Matlab下求解MILP首选Gurobi或Mosek,次选CPLEX。YALMIP作为建模层的好处是语法统一,换求解器只是改一行ops = sdpsettings('solver','gurobi')的事。我自己用的组合是Matlab 2023b + YALMIP + Gurobi 10.x,Gurobi破解版或学术版申请,具体因人而异。
几个实用的求解器设置:
ops = sdpsettings('solver','gurobi','verbose',2); ops.gurobi.MIPGap = 0.01; % 1%的MIP Gap ops.gurobi.TimeLimit = 600; % 600秒上限 ops.gurobi.NumericFocus = 3; % 数值稳定性优先NumericFocus这个参数很少人调,但很有用。两阶段鲁棒模型里,投资成本动辄百万级,运行成本几千块,数值尺度差距巨大,Gurobi的默认尺度处理可能会损失精度。把NumericFocus调到2或3,能有效减少“整数变量但解出来的值是0.9999999”这类问题。
另外,在YALMIP里定义约束时,能用implies就用implies,尤其储能不能同时充放电的约束。但要注意,implies会引入额外二进制变量,规模大的时候反而拖慢求解速度。我实测下来,如果模型只有几百个二进制变量,直接用大M法把非负变量对P_ch和P_dis改成互斥约束更高效:
P_ch(t) <= M * z_ch(t); P_dis(t) <= M * z_dis(t); z_ch(t) + z_dis(t) <= 1;M不需要取得很大,取所有设备额定功率之和的1.2倍就够。M太大会增加数值困难,太小又会误伤解空间,这个度需要自己试。
5. Matlab代码实现:从参数初始化到结果可视化
5.1 数据初始化与参数设计
复现的第一步,不是写代码,而是把参数表整理清楚。我建议在代码开头用一个结构体把所有参数集中管理,别散落在各处——调试的时候散落的参数最让人头大。
% 参数结构体定义 par.t = 24; % 调度时段数(小时) par.dt = 1; % 单位时段(h) par.node_num = 5; % 微网节点数(视拓扑而定) % 负荷数据(数据中心的典型日负荷曲线) par.load_IT = [0.6 0.6 0.55 0.55 0.5 0.5 0.55 0.7 0.8 0.9 0.95 1.0 ... 0.95 0.9 0.85 0.8 0.85 0.9 0.95 0.9 0.8 0.7 0.65 0.6] * 1e3; % kW % 光伏预测出力(标幺值,乘以额定容量得实际出力) par.pv_pu = [0 0 0 0 0 0.05 0.15 0.35 0.55 0.75 0.85 0.9 ... 0.85 0.75 0.6 0.45 0.3 0.15 0.05 0 0 0 0 0]; % 峰谷平时段划分(购电价,元/kWh) par.price_buy = [0.5 0.5 0.5 0.5 0.5 0.6 0.8 1.1 1.2 1.1 1.0 0.9 ... 0.9 0.9 1.0 1.1 1.2 1.2 1.0 0.8 0.7 0.6 0.5 0.5]; % 不确定集合参数 par.delta_pv = 0.2; % 光伏偏差系数 par.delta_load = 0.1; % 负荷偏差系数 par.Gamma = 6; % 预算不确定参数(调度时段内最多允许6个时段同时达到最恶劣)这里几个设计意图:IT负载曲线在白天和晚间有两个高峰,是因为在线业务流量集中在白天,而夜间有批处理任务,也能压一部分。光伏出力曲线要能反映中午缺失的单调性,与实际出力规律匹配。电价峰谷时段要与数据中心可调节负荷时段错开,鲁棒优化才有收益空间。
5.2 主问题建模与迭代过程中的变量管理
C&CG主问题的YALMIP建模有个技巧:每轮迭代的动态变量怎么声明。主问题要包含所有已经发现的场景。我的做法是自定义一个场景结构数组,每轮迭代把新场景追加进去。
for k = 1:K % 迭代轮次 % 声明当前场景相关的运行变量 P_ch{k} = sdpvar(par.t, 1, 'full'); P_dis{k} = sdpvar(par.t, 1, 'full'); SOC{k} = sdpvar(par.t+1, 1, 'full'); P_buy{k} = sdpvar(par.t, 1, 'full'); % 所有场景共享第一阶段容量变量 C_bess = sdpvar(1, 1, 'full'); x_bess = binvar(1, 1); end主问题的约束分两类:一类是静态约束(所有场景通用),另一类是动态约束(每个场景各自的功率平衡、储能约束)。动态约束需要在循环体里加进去。
SOC(1)是初始荷电状态,我一般设0.5。如果你要让系统每天循环运行而不是只跑一天,可以在末尾加约束SOC(24+1) >= SOC(1),保证一天结束储能回到初始水平。这个约束会显著影响结果,别漏了。
每轮迭代结束时,把本轮子问题找出的最恶劣场景存下来:
scenarios{k}.pv = pv_worst; % 最恶劣光伏出力场景 scenarios{k}.load = load_worst; % 最恶劣负荷场景 scenarios{k}.price = price_worst;% 最恶劣电价场景主问题求解时,这些历史场景的约束要一直保留,这是C&CG正确性的保证——切掉任何一个历史最恶劣场景,UB和LB都可能不收敛。
5.3 子问题的max-min结构怎么线性化
子问题固定了第一阶段投资容量后,形式是max_u min_y。直接求解这个两层结构依旧困难,常见解法是:
- 内层min是LP(当第二阶段无整数变量时),取对偶变成max,于是子问题变成max_u max_λ(对偶变量)的结构,合并成一个max问题;
- 但不确定度和对偶变量相乘,产生双线性项,这个双线性项可以通过强对偶条件和大M法线性化;
- 更实用的是,在Matlab里直接调用
uncertain()和robust求解。
我用YALMIP的鲁棒优化模块试过,发现对规模小的算例可以,但一旦节点多、时段长,求解器处理不确定声明的预处理会很慢。所以复现论文时,我都是手写线性化。基本思路是:
- 写出内层min问题的对偶问题;
- 强对偶条件a^T y = b^T λ,由此把max-min的目标值转换成可计算的表达式;
- 把不确定变量与对偶变量的乘积项,用McCormick包络或大M法线性化。
这个过程容易出错,建议每步验证一下:先固定数据,把外层的max去掉,手工检查对偶问题的目标和约束与原LP的目标和约束是否一致。这个验证比什么都重要。
5.4 收敛性判断与可视化输出
C&CG循环中,每一轮迭代主问题的目标函数是下界,子问题目标函数加上当前投资成本后是上界。收敛判据写成:
if abs(UB - LB) / abs(LB) < 1e-3 break; end注意当LB很小时,除以一个很小的数会放大误差,所以绝对误差和相对误差可以结合使用,或者直接用abs(UB - LB) <= 1e-3。
迭代收敛后输出三张图:
- 各设备容量配置柱状图(光伏、储能、柴油机、燃气轮机),用于对比不同Gamma取值下的配置差异;
- 最恶劣场景下的24小时调度曲线,展示储能SOC曲线、购电功率、光伏出力、IT负载调节情况,这张图最好画成堆叠面积图,能很直观看到各电源的出力占比;
- 上下界收敛曲线,横轴是迭代次数,纵轴是成本,一条阶梯下降(UB),一条阶梯上升(LB),两条线夹逼到同一个值。这张图直接放论文的仿真章节,审稿人一看就懂。
6. 工具链与复现环境准备
6.1 Matlab环境配置与工具箱依赖
做这个项目,Matlab版本选择很关键。我用过2020a到2023b,整体体验是2023b对YALMIP和Gurobi的兼容性最省心。Matlab 2023a到2023b之间有个优化工具箱接口的变化,老版本YALMIP可能报“Unable to detect solver”之类的问题,新版本基本没这个毛病。如果你用的是Matlab 2022b或更老版本,遇到奇奇怪怪的求解器兼容问题,别急着改代码,先去检查YALMIP版本和求解器接口版本是否匹配。很多“代码跑不通”的报错源头其实是环境兼容,不是算法写错。
工具箱方面,必须有优化工具箱,YALMIP本身依赖它做默认求解器兜底。如果装的是精简版Matlab,很可能缺了建模工具箱、并行计算工具箱,这时候YALMIP的某些函数会调用失败。
Linux服务器上装Matlab更是要严格按顺序装:先装核心Matlab,再装工具箱包,然后手动激活。如果激活失败,常见报错是license manager error -8,多数是mac地址绑定错误或者license路径没写对。需要在activate工具里重新指定license文件。有时候还会遇到中文乱码问题——Linux上Matlab默认编码是UTF-8,Windows上默认是GBK,代码文件在Windows下用ANSI编码保存的中文注释,拿到Linux上会乱成一团。解决方法是统一用UTF-8编码保存所有.m文件,Matlab 2020b之后在“预设项-常规-文件编码”里可以指定。
6.2 求解器选型与性能对比
在这个项目里,求解器的性能直接影响你的复现效率。我的实测数据:一个5节点、24时段、3类不确定参数的算例,C&CG迭代10轮左右,主问题MILP规模大概几百个二进制变量、数千个连续变量。Gurobi 10.x跑一轮主问题约10~30秒,整个C&CG流程3~6分钟。
对比一下几个常见求解器:
| 求解器 | MILP求解速度 | 鲁棒优化支持 | 许可成本 | 备注 |
|---|---|---|---|---|
| Gurobi | 最快 | 无内建支持,需手写线性化 | 学术免费/商业付费 | 首选,数值稳定性好 |
| Mosek | 快,尤其连续问题 | 无内建支持 | 学术免费 | 对LP的二次维也处理能力强 |
| CPLEX | 快 | 无内建支持 | 学术免费 | 老牌,接口稳定 |
| MATLAB intlinprog | 慢一个数量级 | 无 | 随Matlab自带 | 小算例验证够用,大规模不建议 |
我的建议:写代码和调试用小算例,用intlinprog就行,方便;最终跑论文结果,用Gurobi。用Gurobi时,记得在YALMIP里显式传参ops.gurobi.OptimalityTol等,YALMIP默认的参数不一定是最优配置。
6.3 快速验证的小算例设计
正式跑全规模算例前,强烈建议先设计一个3节点、6时段的最小算例,用来验证算法正确性。6时段的好处是:人工心算也能验算主要约束,而且C&CG迭代1~2轮就能收敛,方便打断点观察中间结果。
小算例的设计原则:
- 只保留1台光伏、1台储能、1台柴油机;
- IT负载只有一个高峰时段,其他时段平稳;
- 电价只分峰/谷两段;
- 不确定集只考虑光伏一个参数,Gamma取1或2。
先跑通这个小算例,确认C&CG收敛、UB/LB单调变化、各约束都满足,再去扩规模。这样做省下的调试时间至少半天起。
复现中还有一个我多次遇到的坑:把24时段和15分钟间隔搞混。很多论文里用15分钟作为调度步长,但案例数据表又写成24小时平均值,导致功率和能量的单位错位,储能SOC算出来要么超过1要么低于0。我自己的习惯是第一步就把时间颗粒度写进参数表里,所有公式统一用par.dt,不在任何地方硬编码。
7. 复现论文中的常见问题与对策
7.1 场景膨胀导致主问题内存爆炸
C&CG迭代过程中,每轮把最恶劣场景加进主问题,但主问题的规模和迭代轮次线性增长。如果迭代20轮,就是20个场景的约束和变量同时参与求MILP。算例中等规模还好,大型微网(10节点以上、时段96段)可能会在10轮之后明显变慢,甚至内存不足。
对策有几个方向:
- 场景压缩:如果多个历史场景对应的运行约束高度相似,可以考虑只保留“代表性”场景,但这会损失理论保证,审稿人可能不接受;
- 增加收敛判据的容忍度:MIPGap从1e-3放宽到5e-3,能显著减少迭代轮数,代价是结果偏差0.5%左右;
- 给每轮主问题加时间限制:比如600秒,超时返回当前最优整数解而不是继续搜,这样即使不能收敛到全局最优,也能得到可行方案;
- 用Benders分解替代C&CG的一部分约束添加,但实现复杂度会上升。
我实测下来,最有用的是MIPGap放松到1e-3,再结合TimeLimit。精度损失在可接受范围,求解时间能缩短一半以上。
7.2 不确定集参数Gamma的取值范围怎么定
Gamma取值太小,鲁棒性体现不出来,结果和确定性差不多;取值太大,系统设计过于保守,储能光伏容量严重冗余,运行成本高得离谱。
推荐做法:先跑Gamma=0的确定性基线和Gamma=10的极端鲁棒,对比两个方案下的成本差和容量差,再在之间等距取5~7个点画灵敏度曲线。如果曲线在Gamma=4到6之间出现明显拐点,那论文讨论就可以聚焦在这个区间。
值得注意的是Gamma的物理意义和不确定参数的个数直接相关。比如有24个时段的光伏不确定参数,Gamma=6意味着允许最多6个时段的光伏出力同时遭最恶劣偏差,也就是最不利时间段的四分之一。Gamma这个口径一定要在论文表述里写清楚,别让审稿人猜。
7.3 数据单位与量纲的常见陷阱
还有几个我和学生复现时反复中招的单位问题:
- 功率用kW,但电价按元/kWh,计算购电成本时是功率乘以电价再乘以时间步长(小时);
- 储能容量用kWh,但不能直接和功率约束比较,要先乘额定功率或时长再比较;
- 碳排放成本如果按tCO2算,注意燃料的排放因子乘以出力(kW)和时间(h),再除以1000换算成吨;
- 如果给的是标幺值(p.u.),要清楚基准容量是多少MW,否则所有结果都是错的。
安全起见,你可以在代码里写一个断言检查:
assert(max(SOC_opt) <= 1.01 && min(SOC_opt) >= -0.01, 'SOC越界');每一轮迭代都检查一遍结果,越界就停下来回头查模型。做仿真的,数据校验的功夫省不了。
7.4 和论文结果对不上怎么处理
EI复现最让人头疼的就是:跑了老半天,出来的成本和论文里的数值差一大截,有时根本不在一个量级。这时候可以从几个角度排查:
- 参数口径:论文里是否包含了折旧系数、通货膨胀率、残值?有些论文的投资成本是等年值,有些是总现值,转成同一个口径再比;
- 时间范围:规划年限是10年还是20年,折现率是8%还是5%,对最终成本的量级影响很大;
- 不确定集合几何:论文用的是盒式还是预算,Gamma取值多少,差一个Gamma就可能差10%以上的成本;
- 基础假设:光伏投资成本是不是考虑了补贴?储能是不是按倍率(比如2C)配置的?这些细节论文里常一笔带过,复现时全是坑。
我的建议是,不要追求和论文数值完全一致,重点是复现“趋势”。只要容量配置随Gamma增加的趋势一致、成本曲线形状一致、不同方案优劣排序一致,你的复现就算成功了。数值有个5%~15%的偏差在论文复现里太正常了。
8. 最后再分享一点个人体会
做这个方向的复现,我前前后后写了不下五个版本,从最开始的确定性优化,到盒式鲁棒,到预算鲁棒,再到C&CG,每一步都踩了不少坑。印象最深的一次,是子问题对偶变换漏了一个非负变量约束,结果C&CG迭代了20轮都不收敛,UB和LB越拉越开,最后发现是松了对偶变量的符号约束,导致对偶问题不可行。从那以后,我每次写完对偶都要先跑随机数据验证一遍,再进主流程。
另外,Matlab代码的工程化也值得下点功夫。我见过很多复现代码,变量全是a、b、tmp,根本没法维护。建议从一开始就用有意义的变量名:P_ch_ess(储能充电功率)、P_dch_ess(储能放电功率)、SOC_ess(储能荷电状态)、P_pv_curtail(光伏弃光量)。多写几个字母不累,但调试的时候能省你几个小时。
如果你也想复现这个论文,可以按这样的学习路径走:第一步,把确定性两阶段模型建起来,跑通一个24时段算例,这是基本功;第二步,加盒式不确定集,用枚举法手算最恶劣场景,理解鲁棒的意义;第三步,换预算不确定集,实现C&CG,画出收敛曲线;第四步,逐步添加灵活性建模(时移负载、UPS、蓄冷),对比不同灵活资源对规划结果的影响。每一步都能产出有意义的结果,也都能单独成文。这样按阶段推进,比一口气想复现整个论文要稳得多。
以后有机会,我再写一篇专门讲怎么把C&CG扩展成多阶段鲁棒或者分布鲁棒的文章。这是一个越研究越有嚼头的方向,建模和算法都有很多可以挖的细节。