news 2026/9/10 9:00:55

考虑储能容量衰减的虚拟电厂多时间尺度调度复现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
考虑储能容量衰减的虚拟电厂多时间尺度调度复现

最近我扎进了一项顶级SCI复现工作:虚拟电厂的多时间尺度调度。论文的核心思路一句话就能概括——在考虑储能系统容量衰减的前提下,把发电侧出力与多用户负荷的灵活性放进同一个优化框架,用Matlab实现"日前+日内+实时"的分层调度。但真动手复现的时候你会发现,标题越短,暗坑越多。

先说清楚这篇博文能给你什么。如果你正准备复现类似方向的工作(虚拟电厂、微电网调度、储能容量衰减建模、需求响应),或者你手里有论文但不知道怎么从公式走到可运行代码,这篇文章就是我的完整路线和纠错记录。每一段都会回答一个"为什么",而不仅仅是告诉你"怎么敲"。所有代码片段我都按YALMIP+Gurobi的常用组合给出,其他求解器比如CPLEX也可以直接替换,本质是一样的。

1. 虚拟电厂为什么会同时需要"多时间尺度"和"灵活性"两个概念

1.1 不同时间尺度下,调度决策面对的不是同一个问题

虚拟电厂这个概念听起来很"大",工程上其实就是一句话:把分散的分布式光伏、风电、储能和可控负荷聚合起来,从电网的视角看,它像一个可控的发电厂。光伏和风电出力由天气决定,储能是缓冲,负荷则一直在变,这些对象叠加在一起,运行状态存在强不确定性。如果只做一个静态的最优调度,第二天实际天气和昨天预测差一截,计划就完全失效。

所以论文采用多时间尺度调度,本质是为了"分步兑付"不确定性。日前的决策是粗粒度的,只能决定哪些机组开、储能大致怎么用、负荷基线怎么分配;日内滚动决策用最新预测修正储能出力;实时调整则只负责在几分钟到一刻钟的尺度内应对剩余误差。三个尺度上,变量类型不同,输入数据不同,代码实现上不是一个大模型算完,而是三个小模型按时间顺序调用。

我在复现时最大的感受是,多时间尺度最核心的不是公式本身,而是每个阶段的信息边界。日前阶段不能"偷看"日内阶段的新息,这是论文里隐含的因果约束。代码上也必须严格按时间顺序逐阶段求解,把上一阶段的决策结果作为下一阶段的输入,这样才能复现出原文的时间递进逻辑。很多初学者一上来就把三个阶段的决策变量全部放在一个模型里联合优化,出来的结果看起来更"优",但实际违背了滚动优化的前提,在复现SCI工作时属于硬伤。

1.2 用户负荷的"灵活性"到底指什么,怎么量化

多用户负荷灵活性是这篇论文里不太好啃的一块。它指的不是"用户用多少电",而是"用户在多大程度上允许你调整他的用电时间和用电量"。比如工厂的某条生产线可以提前一小时启动,空调的设定温度可以在舒适区间内浮动,电动汽车的充电时间可以平移——这些就是可平移负荷、可削减负荷、可延迟负荷。

量化灵活性,我建议按三类参数来做:

  • 可削减比例:在基线负荷基础上,用户允许减载的上限比例,比如30%;
  • 可平移时段:允许负荷从原计划时段平移到其他时段的时间窗,比如前后2小时;
  • 补偿价格:用户因配合调度而产生的单位补偿成本,这是目标函数里要用的权重。

多用户与"单用户"的区别在于,论文里不是给每个用户单独建一个优化问题,而是把所有用户的灵活性参数聚合成一个可调用集合。聚合之后,灵活性转化为一个调度模块:系统需要削减时,按补偿价格从低到高调用;需要平移时,受时间窗约束。实现时我用了一个"用户类型+乘数"的方式,比如一个用户类型代表500户同参数的用户,乘数乘以户数得到总潜力。这样既保证模型规模可控,又能还原论文里"多用户"的含义。

1.3 储能容量衰减:一个容易被忽略但足够致命的长周期变量

储能容量衰减为什么值得放进调度模型?因为电池不是"用完就扔"的理想设备。锂离子电池每一次充放电,都会带来不可逆的活性物质损失,放电深度越深、循环次数越高,容量衰减越快。如果调度模型完全不考虑衰减,优化器为了让当下购电成本最低,会让储能频繁深充深放,短期账算下来了,但电池寿命会被大幅压缩,很快变得"不好用",长期经济账其实是亏的。

数学上,容量衰减可以建模为多种形式。最常用的是把循环寿命与放电深度挂钩的经验公式:每一次充放电循环造成的容量损失,是放电深度DOD的函数。这个函数通常是非线性的。论文标题里特别强调"在考虑储能系统容量衰减的同时",说明衰减不是事后算账,而是嵌在优化模型内部的约束或目标项。

我在代码里做的处理是,把衰减造成的影响转化成一个"容量折算系数"或"衰减惩罚成本"。容量折算系数更新储能可用容量范围,衰减惩罚成本则进入目标函数。这样一来,优化器在决定是否深充深放时,会同时看到"今天省下的购电费"和"电池未来的寿命损失",策略自然就变得温和。你可以把衰减惩罚理解成给储能使用上了一道无形的"租金"——用得多、用得狠,代价就高,系统自然会掂量。

2. 论文模型的核心拆解:多时间尺度调度如何组织

2.1 日前计划、日内滚动、实时调整:三层结构的典型分配

论文里的三层时间尺度,每层有不同的任务。为了后面代码部分方便对齐,我先把三层结构和各自变量列成一个表格。

阶段时间粒度决策范围主要输入输出
日前计划1小时或15分钟全天24小时短期预测曲线、用户灵活性申报机组启停、储能逐时出力计划、负荷调用计划
日内滚动15分钟未来2-4小时最新预测数据、日前计划储能出力修正量、负荷调用修正量
实时调整5-15分钟未来15分钟到1小时超短期预测、实测数据储能快速出力、备用容量调用

这套结构的关键在于,每个阶段都会沿用上一阶段的决策结果作为基准。日前计划给出的储能计划不会是"必须严格执行的指令",而是日内滚动的参考基线;日内滚动修正后的计划又作为实时阶段的目标。三层之间的信息是逐步"细化-修正-补救"的关系。

代码层面,我建议用三个函数分别封装三层调度,例如 day_ahead_schedule()、intraday_rolling()、real_time_adjust(),每个函数接受上一阶段的输出结构和最新预测数据,返回本阶段的决策结果。这样结构清晰,也方便单独调试某一层。

2.2 目标函数怎么设:经济性、衰减惩罚、弃电惩罚

我在复现时按论文常规写法把目标函数拆成了几块。最小化系统总运行成本:

  • 从电网购电的费用;
  • 分布式光伏、风电的运维成本(通常很小,但论文里会算);
  • 调用用户负荷灵活性的补偿成本;
  • 储能容量衰减带来的经济损失;
  • 弃风弃光惩罚项,防止优化器毫无意义地抛掉新能源出力。

每一块的量纲都是"元",权重系数从几块钱到几十块钱不等。如果完全不设惩罚项,优化器有可能会用"弃掉所有光伏"这种荒谬的方案来规避储能调度成本,所以弃电惩罚其实是一种"引导性成本",让结果更贴近实际运行。

衰减惩罚项具体怎么进目标函数,不同论文差异很大。有的论文做成储能循环次数乘以单次退化成本;有的做成当前容量下降导致的平均发电成本上升。复现时不必完全照抄,关键是让优化器"感受到"衰减的代价。我在代码里用的是"每MWh储能吞吐量折算的成本系数",这个系数来自容量衰减经验模型的归因结果,简洁且可以解释。

目标函数写出来大概长这样:

objective = sum(c_buy .* P_buy - c_sell .* P_sell) ... % 购售电费 + sum(sum(flex_compensation .* P_call)) ... % 灵活性补偿 + deg_cost_per_mwh * sum(P_ch + P_dis) ... % 衰减惩罚 + curtail_penalty * sum(P_pv_avail - P_pv_used); % 弃光惩罚

需要注意,P_pv_used才是真正被消纳的光伏功率,P_pv_avail是最大可用出力,两者差值就是弃光量。我记得第一次复现时把这两个量混了,导致弃光惩罚项恒等于零,优化结果一路把光伏全弃了,查了很久才发现。

2.3 约束条件清单:功率平衡、SOC递推、爬坡、容量衰减更新

目标函数之外,约束条件是这个复现工作的重头戏。我按每个阶段的公共部分列一下:

  • 功率平衡约束:虚拟电厂与电网的交换功率加上光伏功率、风电功率、储能放电功率,要等于负荷用电加上储能充电功率。每个时间断面都成立。
  • 储能SOC递推约束:SOC(t+1) = SOC(t) + 充电功率×充电效率/容量 - 放电功率/(容量×放电效率)。这个递推关系在代码里用一组线性等式表达。
  • 储能充放电功率上下限约束、SOC上下限约束。注意如果考虑容量衰减,SOC的上限不是固定值,而是随容量损失递减。
  • 储能充放电互斥约束:同一个时刻不能同时充电又放电。YALMIP里用二进制变量实现。
  • 用户灵活性约束:每个用户的实际负荷不能超过基线加可平移量,也不能低于基线减可削减量,时间窗内平移到其他时段的负荷总量要保持前后一致。
  • 备用约束:实时阶段要留有足够的可上调/可下调备用容量,用来应对预测误差。

容量衰减更新约束在论文里往往是动态更新的,形态上可以写成 C_max(t) = C_0×(1 - f(累计循环DOD))。这个函数非线性,在下一节我会讲两种能落地的方式。

3. Matlab代码实现路径:从公式到可运行程序

3.1 建模工具选择:YALMIP + 商业求解器是主流方案

Matlab里搭优化模型,我不推荐手写矩阵A、b然后自己调linprog,因为约束一多,索引混乱会让人崩溃。我复现时用的是YALMIP工具箱,它支持用接近数学公式的语法声明变量和约束,然后底层适配Gurobi或CPLEX求解。这样代码可读性好,改约束也容易。

求解器方面,Gurobi是同类里对MILP(混合整数线性规划)和二次规划处理最稳的,学术版免费。CPLEX也可以,但当年在Matlab里的运行环境配置略微繁琐。如果一时拿不到商业许可,可以先用YALMIP配合开源的GLPK跑小规模测试,但不要指望它能撑起带有几千个二进制变量的完整多时间尺度模型。我实测Gurobi在3000变量规模下秒解,GLPK可能要卡几分钟甚至超内存。

安装YALMIP和Gurobi时注意版本匹配:Gurobi需要对应Matlab版本,启动时在Matlab里要设置好路径,添加环境变量不太够,还要用gurobi_setup命令注册。这一步我在不同电脑上遇到过几次"求解器找不到"的问题,基本都是setup没跑,或者是路径里带了中文导致加载失败。

3.2 数据准备:光伏/风电/负荷曲线、用户灵活性参数怎么造

复现论文最麻烦的不是代码,是数据。论文题目里的"多用户负荷",在公开数据集里通常要用一个标准负荷曲线加点随机波动来生成,我用的是如下思路:

  • 用96点(15分钟一个点)代表一天;
  • 基础负荷曲线用一个典型日负荷曲线加高斯噪声,模拟不同用户群体的差异;
  • 光伏出力用标准光照曲线乘湿度、温度影响系数;
  • 风电出力用威布尔分布风速转换成功率,再加时序相关性。

这里的随机噪声要设置随机种子,不然每次跑出来的结果都不一样,后面不好对照实验。Matlab里rng(42)固定种子,保证论文结果可复现,这是复现工作里很基础也很容易被忽略的一步。

用户灵活性参数我按类型生成,三类用户:居民、商业、工业,参数如下:

用户类型可削减比例上限可平移时间窗单位补偿价格
居民20%±1小时0.3元/kWh
商业30%±2小时0.5元/kWh
工业40%±3小时0.8元/kWh

这些参数不一定和原文一致,但复现时参数的可解释性比精确匹配更重要,因为我们要验证的是模型机制,不是某个特定数据集的答案。实际写代码时,我建议把这三类用户定义成一个结构体数组,每个元素包含base_load、cut_ratio、shift_window、price四个字段。这样后面写灵活性约束时,可以用循环遍历用户类型,代码更清晰。

3.3 求解代码骨架与变量维度管理

下面这段是我日前阶段的核心骨架,YALMIP语法,变量维度统一为T×1向量或T×N矩阵:

T = 96; % 一天96个时间断面 N_user = 3; % 用户类型数 P_buy = sdpvar(T,1); % 每个时段的购电功率 P_sell = sdpvar(T,1); % 售电功率(如果允许倒送) P_ch = sdpvar(T,1); % 储能充电功率 P_dis = sdpvar(T,1); % 储能放电功率 SOC = sdpvar(T+1,1); % SOC轨迹,T+1是因为递推从t到t+1 z_ch = binvar(T,1); % 充电状态 z_dis = binvar(T,1); % 放电状态 P_call = sdpvar(T,N_user); % 每位用户被调用灵活性后的实际功率

约束部分关键几行:

Constraints = []; % 功率平衡 Constraints = [Constraints, P_buy - P_sell + P_pv + P_wind + P_dis == ... P_load(all) + P_ch + sum(P_call,2)]; % 充电/放电互斥与上下限 Constraints = [Constraints, P_ch >= 0, P_ch <= P_ch_max*z_ch]; Constraints = [Constraints, P_dis >= 0, P_dis <= P_dis_max*z_dis]; Constraints = [Constraints, z_ch + z_dis <= 1]; % SOC递推 for t = 1:T Constraints = [Constraints, SOC(t+1) == SOC(t) + ... (P_ch(t)*eta_ch - P_dis(t)/eta_dis) / C_bat]; end

这段代码的核心价值是:变量维度对齐决定了你后面调试的痛苦程度。我强烈建议在每个约束后面加一行注释,写清楚"这个等式右边每一项的行数与列数",出问题时先检查维度,再检查逻辑。

3.4 容量衰减的非线性处理:分段线性化与迭代更新

容量衰减函数通常是非线性的,而规划模型最好保持线性,否则求解难度会急剧上升。这里我提供两种落地方式。

第一种是分段线性化。把放电深度DOD与容量损失率的关系曲线切成若干段,每段用一条线性函数近似,用一组SOS1约束或二进制变量选择段。这种方式精度高,缺点是需要额外引入不少辅助变量,代码量会增加。

第二种是迭代更新,也是我在复现时更常用的方式:先假设储能容量为额定值,求解一遍调度;解出SOC轨迹后,统计每一个充放电循环的DOD,代入衰减经验函数计算出新的容量值;然后用新容量重新求解调度;重复几次直到容量变化小于阈值。这个方案把非线性关系收敛在外层循环里,内层仍是标准MILP,实现简单,结果也能收敛。

三种方式对比:

方案精度实现复杂度求解耗时
直接线性化嵌入
分段线性化中高
外层迭代更新

对于复现论文来说,如果原文没有明确给出衰减函数的内部嵌入方式,用外层迭代更新做验证研究是够用的;如果你想进一步做参数敏感性分析,可以再升级为分段线性化。外层迭代的收敛判断我用的是相对误差小于1%,一般迭代3到5次就能达到。

4. 结果分析与验证:怎么判断你的复现对不对

4.1 设置对照实验:考虑衰减 vs 不考虑衰减

复现结果最容易出问题的地方,其实是"你没法确定算出来的东西是不是对的"。我的做法是先设计对照实验,再定性比较,用趋势合理性做判断。

场景A:不考虑储能容量衰减,只做经济调度;场景B:在目标函数和约束中加入衰减惩罚与容量更新;场景C:B的基础上再放开用户负荷灵活性调用。

预期结果:

  • B的总费用会比A高(因为储能策略变温和,购电成本上升),但电池累计吞吐量和容量损失显著下降;
  • C的总费用会比B低(因为灵活性负荷以更低成本替代了部分储能调度和购电),同时储能压力进一步减轻。

如果场景B算出来总费用反而比A低,或者场景C储能吞吐量反而增加,说明代码里某个约束的方向写反了,或者是衰减惩罚项符号错了。我调试时遇到过的问题是把衰减惩罚写成了负号,导致系统每充放一次电反而"赚"一笔,结果SOC曲线在边界上疯狂弹跳,最后总成本出现负值,一看就是符号错了。

4.2 从SOC轨迹与成本构成中找证据

结果图画出来后,重点看三个地方。

第一个是储能SOC轨迹。考虑衰减时,SOC曲线应当更加平滑、很少触碰上下限,充放电次数更少。如果SOC轨迹频繁弹到0%或100%,说明衰减惩罚设置过低,或者SOC上下限没有正确随容量更新。

第二个是功率平衡曲线。理论上每个时段的购电功率加光伏加风电加放电,等于负荷加充电加用户调用,画完图这条线应该严丝合缝地重合。如果发现曲线有缝隙,基本是约束写漏了,优先检查sum(P_call,2)这一维度是否把三个用户类型的实际功率都加上了。

第三个是成本构成饼图。看购电成本、灵活性补偿成本、衰减成本、弃电惩罚各自占比。灵活性补偿成本不应占大头,否则说明用户补偿价格设置得过高,市场机制失真。我在一次实验里把工业用户的补偿单价设到了2.8元/kWh,结果系统狠命调用工业负荷削减,灵活性补偿成本直接超过了购电总费用,明显不合理。

4.3 指标验证:约束满足度、求解时间与收敛性

我还建议在代码里加一段自动校验段,求解完再验证一次所有约束是否满足,误差容忍度设置在1e-6。这一步看似简单,却能帮你快速过滤掉很多隐蔽的bug。可以在YALMIP里用check(Constraints)直接查看每个约束的残差,也可以通过assign(..., value)把求解结果赋值回去再手动计算,两种方式看个人习惯。

求解时间方面,三阶段模型的总耗时应该控制在几十秒到几分钟的范围内。如果日内滚动阶段耗时超过十分钟,优先怀疑变量二进制数量过多,或者Gurobi的MIPGap参数没有设置。我一般会把MIPGap设为0.01%,TimeLimit设为1800秒,两者配合使用,保证结果既精确又不失控。对于纯线性规划部分,求解时间通常是毫秒级,慢的几乎都在MILP的整数变量上。

5. 复现全过程最容易踩的坑

5.1 变量维度混乱与索引错位

这是我复现时消耗时间最多的一块。多用户负荷用N×T矩阵表示,储能用T×1向量表示,光伏、风电各自是T×1,一旦在约束里做两者的点乘,维度不匹配的报错就会出来。更隐蔽的是它不报错但结果错误——比如sum(P_call,2)从聚合维度写错了方向,把用户类型维度给求和了,结果功率平衡还算得通,但物理意义完全不同。

我的解决习惯是:每个变量名后缀带上维度说明,比如P_call_T_N,代码看着长,但排查效率高很多。同时在约束后打印变量维度,核对无误再继续。还有一个实用技巧:在设计阶段就把T写在脚本最前面,所有矩阵用T去生成,不要出现写死的96,这样后续改成24点或288点只要改一处。

5.2 求解器数值问题与精度设置

不同变量的数量级差别很大,电价可能是0.5元/kWh,储能容量可能是2000kWh,目标函数数值跨了好几个数量级。这种情况对求解器的数值稳定性很不友好,尤其是MILP,动不动就出现很不合理的解。

应对办法是归一化。把功率、容量、价格全部除以基准值,比如功率基准100kW,价格基准1元/kWh,让所有变量落在0.01到100的范围内。这个操作对结果只是缩放,不会改变最优解结构,但能让求解器稳定很多。归一化之后,Gurobi的默认数值参数就够了,基本不用再去调NumParam。没有归一化之前,我遇到过两次求解器直接返回inf或者数值非常离谱的解,归一化之后一切正常。

5.3 参数敏感性:衰减系数、罚因子怎么定

衰减系数和罚因子是模型里最容易让结果"变脸"的参数。同一套代码,衰减系数从0.0001改成0.01,场景B可能从"电池基本闲置"变成"电池深充深放",结论完全反过来。

我建议的做法是先跑一组参数扫描,画两条曲线:横轴是衰减系数,纵轴分别是总成本和储能吞吐量。如果曲线突变点很多,说明模型稳定性差,优先检查衰减函数的拟合区间是否超出合理范围,再检查是否有约束被过早激活。最终选在曲线平缓区域的参数,这样论文里的结论才经得起审稿人质疑。

最后给你一个调试顺序建议:先复现"不考虑衰减"的基础经济调度版本,确认所有约束满足、结果符合物理直觉;再加上灵活性负荷,验证它确实降低了总成本;最后再加容量衰减。每一层都验证通过,再往前推进。这个顺序我用了很多次,能最大程度上把"模型逻辑错误"和"代码实现错误"区分开,也是我接手任何复现类任务时固定的起手式。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 8:58:59

Matplotlib绘图实战:从函数图像到数据分析可视化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 8:57:40

主动配电网两阶段鲁棒优化故障恢复:Matlab+CCG实现详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 8:51:35

AI应用开发必备Python基础:从环境到实战的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华